# Anthropic Developer Documentation - Full Content This file provides comprehensive documentation with full rendered content. ## Root URL Claude Developer Platform Console (Requires login) https://platform.claude.com ## Available Languages on Website The full documentation is available in the following languages on https://platform.claude.com/docs: - English (en) - 566 pages - ✓ Full content included below - German (Deutsch) (de) - 206 pages - Visit website for content - Spanish (Español) (es) - 206 pages - Visit website for content - French (Français) (fr) - 206 pages - Visit website for content - Italian (Italiano) (it) - 206 pages - Visit website for content - Japanese (日本語) (ja) - 206 pages - Visit website for content - Korean (한국어) (ko) - 206 pages - Visit website for content - Portuguese (Português) (pt-BR) - 206 pages - Visit website for content - Russian (Русский) (ru) - 206 pages - Visit website for content - Chinese Simplified (简体中文) (zh-CN) - 206 pages - Visit website for content - Chinese Traditional (繁體中文) (zh-TW) - 206 pages - Visit website for content - Indonesian (Bahasa Indonesia) (id) - 206 pages - Visit website for content --- # English Documentation - Full Content ## Docs home --- title: Documentation url: https://platform.claude.com/docs/en/home description: Claude API Documentation --- Quickstart Get API key API reference Quickstart API reference Client SDKs Quickstart API reference Define your agent Amazon Bedrock Google Cloud Microsoft Foundry Quickstart Get API key Choose a model Install an SDK Try the Workbench Messages API Thinking Vision Tool use Web search Code execution Structured outputs Prompt caching Streaming Prompting best practices Run evals Batch testing Safety and guardrails Rate limits and errors Cost optimization Workspaces and admin API key management Usage monitoring Model migration Quickstart Get API key Build in Console Agent setup Tools Tool permissions Streaming and events Sessions API reference Workspaces and admin API key management Usage monitoring Interactive courses to master Claude. Code samples and patterns. Deployable starter apps. Latest features and updates. An agentic coding assistant in your terminal. ## Messages ### First steps --- title: Get started with Claude url: https://platform.claude.com/docs/en/get-started description: Make your first API call to Claude and build a simple web search assistant. --- ## Prerequisites * A [Claude Console account](https://platform.claude.com) * An [API key](https://platform.claude.com/settings/keys) ## Call the API Export your API key as an environment variable. The cURL command below reads it from `$ANTHROPIC_API_KEY`. ```bash export ANTHROPIC_API_KEY="your-api-key-here" ``` Send a `POST` request to the Messages API: ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-opus-5", "max_tokens": 1000, "messages": [ { "role": "user", "content": "What should I search for to find the latest developments in renewable energy?" } ] }' ``` Claude returns a JSON response containing the assistant's message: ```json Output { "model": "claude-opus-5", "id": "msg_013mHbppMPd2PrVJzGMZPt2D", "type": "message", "role": "assistant", "content": [ { "type": "text", "text": "Here are some effective search strategies to find the latest developments in renewable energy:\n\n## General Search Terms\n- \"Renewable energy news 2025\"\n- ..." } ], "stop_reason": "end_turn", "stop_sequence": null, "stop_details": null, "usage": { "input_tokens": 21, "output_tokens": 305 } } ``` Install the Anthropic CLI with Homebrew: ```bash brew install anthropics/tap/ant ``` For other installation methods, see [Installation](https://platform.claude.com/docs/en/cli-sdks-libraries/cli/quickstart#installation) in the CLI quickstart. Log in with your Anthropic account: ```bash ant auth login ``` This opens a browser-based OAuth flow. After authorizing, confirm your credential with: ```bash ant auth status ``` On a remote host without a browser, pass `--no-browser` to get a URL you can open on another device, then paste the returned code back into the terminal. If `ANTHROPIC_API_KEY` is set in your environment, it takes precedence over the login credentials. For non-interactive environments such as CI, see [CLI authentication options](https://platform.claude.com/docs/en/cli-sdks-libraries/cli/authentication). Run `ant messages create` from your terminal: ```bash CLI ant messages create \ --model claude-opus-5 \ --max-tokens 1000 \ --message '{ role: user, content: "What should I search for to find the latest developments in renewable energy?" }' ``` The CLI prints the JSON response: ```json Output { "model": "claude-opus-5", "id": "msg_01N1ycuCkM5Mzd7WhTU4fwST", "type": "message", "role": "assistant", "content": [ { "type": "text", "text": "Here are some effective search strategies to find the latest developments in renewable energy:\n\n## General Search Terms\n- \"Renewable energy news 2025\"\n- ..." } ], "stop_reason": "end_turn", "stop_sequence": null, "stop_details": null, "usage": { "input_tokens": 21, "output_tokens": 305 } } ``` Export your API key as an environment variable. The SDK reads `ANTHROPIC_API_KEY` automatically. ```bash export ANTHROPIC_API_KEY="your-api-key-here" ``` ```bash mkdir claude-quickstart && cd claude-quickstart python3 -m venv .venv && source .venv/bin/activate pip install anthropic ``` Create a file called `quickstart.py`: ```python Python import anthropic client = anthropic.Anthropic() message = client.messages.create( model="claude-opus-5", max_tokens=1000, messages=[ { "role": "user", "content": "What should I search for to find the latest developments in renewable energy?", } ], ) for block in message.content: if block.type == "text": print(block.text) ``` ```bash python quickstart.py ``` ```text Output wrap Here are some effective search strategies to find the latest developments in renewable energy: ## General Search Terms - "Renewable energy news 2025" - ... ``` Export your API key as an environment variable. The SDK reads `ANTHROPIC_API_KEY` automatically. ```bash export ANTHROPIC_API_KEY="your-api-key-here" ``` ```bash mkdir claude-quickstart && cd claude-quickstart npm init -y npm pkg set type=module npm install @anthropic-ai/sdk ``` Create a file called `quickstart.ts`: ```typescript TypeScript import Anthropic from "@anthropic-ai/sdk"; const client = new Anthropic(); const message = await client.messages.create({ model: "claude-opus-5", max_tokens: 1000, messages: [ { role: "user", content: "What should I search for to find the latest developments in renewable energy?" } ] }); for (const block of message.content) { if (block.type === "text") { console.log(block.text); } } ``` ```bash npx tsx quickstart.ts ``` ```text Output wrap Here are some effective search strategies to find the latest developments in renewable energy: ## General Search Terms - "Renewable energy news 2025" - ... ``` Export your API key as an environment variable. The SDK reads `ANTHROPIC_API_KEY` automatically. ```bash export ANTHROPIC_API_KEY="your-api-key-here" ``` Create a new console project and add the Anthropic package: ```bash dotnet new console -n ClaudeQuickstart cd ClaudeQuickstart dotnet add package Anthropic ``` Replace the contents of `Program.cs`: ```csharp C# using Anthropic; using Anthropic.Models.Messages; var client = new AnthropicClient(); var message = await client.Messages.Create(new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1000, Messages = [ new() { Role = Role.User, Content = "What should I search for to find the latest developments in renewable energy?", }, ], }); foreach (var block in message.Content) { if (block.TryPickText(out var textBlock)) { Console.WriteLine(textBlock.Text); } } ``` ```bash dotnet run ``` ```text Output wrap Here are some effective search strategies to find the latest developments in renewable energy: ## General Search Terms - "Renewable energy news 2025" - ... ``` Export your API key as an environment variable. The SDK reads `ANTHROPIC_API_KEY` automatically. ```bash export ANTHROPIC_API_KEY="your-api-key-here" ``` Create a new module and add the Anthropic SDK: ```bash mkdir claude-quickstart && cd claude-quickstart go mod init claude-quickstart go get github.com/anthropics/anthropic-sdk-go ``` Create a file called `main.go`: ```go Go package main import ( "context" "fmt" "log" "github.com/anthropics/anthropic-sdk-go" ) func main() { client := anthropic.NewClient() message, err := client.Messages.New(context.Background(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1000, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("What should I search for to find the latest developments in renewable energy?")), }, }) if err != nil { log.Fatal(err) } for _, block := range message.Content { if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok { fmt.Println(textBlock.Text) } } } ``` ```bash go run . ``` ```text Output wrap Here are some effective search strategies to find the latest developments in renewable energy: ## General Search Terms - "Renewable energy news 2025" - ... ``` Export your API key as an environment variable. The SDK reads `ANTHROPIC_API_KEY` automatically. ```bash export ANTHROPIC_API_KEY="your-api-key-here" ``` You need a JDK (25 or later) and either [Gradle](https://gradle.org/install/) or [Maven](https://maven.apache.org/install.html) on your `PATH`. Create a directory for your project with a Java source directory inside it: ```bash mkdir -p claude-quickstart/src/main/java && cd claude-quickstart ``` Then add a build file. Find the current SDK version on [Maven Central](https://central.sonatype.com/artifact/com.anthropic/anthropic-java). Save this as `build.gradle.kts`: ```kotlin plugins { application } repositories { mavenCentral() } java { toolchain { languageVersion = JavaLanguageVersion.of(25) } } dependencies { implementation("com.anthropic:anthropic-java:2.53.0") } application { mainClass = "QuickStart" } ``` Save this as `pom.xml`: ```xml 4.0.0 com.example quickstart 1.0-SNAPSHOT 25 UTF-8 com.anthropic anthropic-java 2.53.0 ``` Save this as `QuickStart.java` in your project's Java source directory (usually `src/main/java/`): ```java Java import com.anthropic.client.okhttp.AnthropicOkHttpClient; import com.anthropic.models.messages.Message; import com.anthropic.models.messages.MessageCreateParams; import com.anthropic.models.messages.Model; static void main() { var client = AnthropicOkHttpClient.fromEnv(); var params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1000) .addUserMessage( "What should I search for to find the latest developments in renewable energy?" ) .build(); Message message = client.messages().create(params); for (var block : message.content()) { block.text().ifPresent(textBlock -> IO.println(textBlock.text())); } } ``` ```bash gradle run ``` ```bash mvn compile exec:java -Dexec.mainClass=QuickStart ``` ```text Output wrap Here are some effective search strategies to find the latest developments in renewable energy: ## General Search Terms - "Renewable energy news 2025" - ... ``` Export your API key as an environment variable. The SDK reads `ANTHROPIC_API_KEY` automatically. ```bash export ANTHROPIC_API_KEY="your-api-key-here" ``` ```bash mkdir claude-quickstart && cd claude-quickstart composer require "anthropic-ai/sdk" "guzzlehttp/guzzle:^7" ``` Create a file called `quickstart.php`: ```php PHP messages->create( model: Model::CLAUDE_OPUS_5, maxTokens: 1000, messages: [ [ 'role' => 'user', 'content' => 'What should I search for to find the latest developments in renewable energy?', ], ], ); foreach ($message->content as $block) { if ($block instanceof TextBlock) { echo $block->text . PHP_EOL; } } ``` ```bash php quickstart.php ``` ```text Output wrap Here are some effective search strategies to find the latest developments in renewable energy: ## General Search Terms - "Renewable energy news 2025" - ... ``` Export your API key as an environment variable. The SDK reads `ANTHROPIC_API_KEY` automatically. ```bash export ANTHROPIC_API_KEY="your-api-key-here" ``` ```bash mkdir claude-quickstart && cd claude-quickstart bundle init bundle add anthropic ``` Create a file called `quickstart.rb`: ```ruby Ruby require "anthropic" client = Anthropic::Client.new message = client.messages.create( model: Anthropic::Model::CLAUDE_OPUS_5, max_tokens: 1000, messages: [ { role: "user", content: "What should I search for to find the latest developments in renewable energy?" } ] ) message.content.each do |block| puts block.text if block.type == :text end ``` ```bash bundle exec ruby quickstart.rb ``` ```text Output wrap Here are some effective search strategies to find the latest developments in renewable energy: ## General Search Terms - "Renewable energy news 2025" - ... ``` ## Next steps You made your first API call. Next, learn the Messages API patterns you'll use in every Claude integration. Learn multi-turn conversations, system prompts, stop reasons, and other core patterns. Once you're comfortable with the basics, explore further: Compare Claude models by capability and cost. Browse all Claude capabilities: tools, context management, structured outputs, and more. Reference documentation for Python, TypeScript, C#, and other client libraries. Compare API keys and Workload Identity Federation, and set key expiration. --- title: Intro to Claude url: https://platform.claude.com/docs/en/intro description: Claude is a highly performant, trustworthy, and intelligent AI platform built by Anthropic. Claude excels at tasks involving language, reasoning, analysis, coding, and more. --- The latest generation of Claude models: **Claude Fable 5** - Next-generation intelligence for long-running agents. Read the [Claude Fable 5 and Claude Mythos 5 announcement](https://www.anthropic.com/news/claude-fable-5-mythos-5). **Claude Mythos 5** - Shares Claude Fable 5's capabilities without the safety classifiers. Available in limited release through [Project Glasswing](https://anthropic.com/glasswing). **Claude Opus 5** - For complex agentic coding and enterprise work. Read the [Claude Opus 5 announcement](https://www.anthropic.com/news/claude-opus-5). **Claude Sonnet 5** - Frontier intelligence at scale, built for coding, agents, and enterprise workflows. Read the [Claude Sonnet 5 announcement](https://www.anthropic.com/news/claude-sonnet-5). **Claude Haiku 4.5** - Fastest model with near-frontier intelligence. Read the [Claude Haiku 4.5 announcement](https://www.anthropic.com/news/claude-haiku-4-5). Looking to chat with Claude? Visit [claude.ai](https://claude.ai). Anthropic offers two ways to build with Claude, each suited to different use cases: | | Messages API | Claude Managed Agents | | -------------- | ------------------------------------------- | ------------------------------------------------------------------------- | | **What it is** | Direct model prompting access | Pre-built, configurable agent harness that runs in managed infrastructure | | **Best for** | Custom agent loops and fine-grained control | Long-running tasks and asynchronous work | To learn more about each, see [Using the Messages API](https://platform.claude.com/docs/en/build-with-claude/working-with-messages) and the [Claude Managed Agents overview](https://platform.claude.com/docs/en/managed-agents/overview). ## Recommended path for new developers Follow these steps to go from zero to a working Claude integration. Set up your environment, install an SDK, and send your first message to Claude. [Go to the quickstart](https://platform.claude.com/docs/en/get-started) Set an expiration when you create your API key. Keep the key out of source control, client-side code, and prompts. Check whether your workload can use Workload Identity Federation instead of a static key. [Read the authentication guide](https://platform.claude.com/docs/en/manage-claude/authentication) Learn the core request and response structure, including multi-turn conversations, system prompts, and stop reasons. [Read the Messages API guide](https://platform.claude.com/docs/en/build-with-claude/working-with-messages) Compare Claude models by capability and cost to pick the best fit for your use case. [See the models overview](https://platform.claude.com/docs/en/about-claude/models/overview) Discover what Claude can do: extended thinking, web search, file handling, structured outputs, and more. [Browse the features overview](https://platform.claude.com/docs/en/build-with-claude/overview) *** ## Develop with Claude Anthropic provides developer tools to help you build and scale applications with Claude. Prototype and test prompts in your browser with the Workbench. Explore the full Claude API and client SDK documentation. Learn with interactive Jupyter notebooks covering PDFs, embeddings, and more. *** ## Key capabilities Claude can assist with many tasks that involve text, code, and images. Summarize text, answer questions, extract data, translate text, and explain and generate code. Process and analyze visual input and generate text and code from images. *** ## Support Find answers to frequently asked account and billing questions. Check the status of Anthropic services. --- title: Authentication url: https://platform.claude.com/docs/en/manage-claude/authentication description: Authenticate to the Claude API with API keys, Workload Identity Federation, or App Attest. --- The Claude API supports three ways to authenticate requests: | Method | Credential | Best for | | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | [API key](https://platform.claude.com/docs/en/manage-claude/authentication#api-keys) | Static `sk-ant-api...` secret in the `x-api-key` header | Local development, prototyping, scripts, and single-tenant servers where you control secret storage | | [Workload Identity Federation](https://platform.claude.com/docs/en/manage-claude/authentication#workload-identity-federation) | Short-lived bearer token exchanged from your identity provider's identity token | Production workloads on cloud platforms (AWS, Google Cloud, Azure), CI/CD pipelines, and Kubernetes, where you want to eliminate static secrets | | [App Attest](https://platform.claude.com/docs/en/manage-claude/authentication#app-attest) | Short-lived access token issued to a genuine, attested installation of your registered iOS or macOS app | iOS and macOS apps distributed to end users, where the app calls the Claude API directly with no back end or proxy | API keys and Workload Identity Federation grant the same access to Claude API endpoints. Choose API keys to get started quickly, and move to Workload Identity Federation when your workload already has a platform-issued identity you can federate. Use App Attest for iOS and macOS apps you distribute to end users. ## API keys API keys are static secrets that you generate in the Claude Console and pass on every request. * **Create a key:** Go to [Settings → API keys](https://platform.claude.com/settings/keys) in the Claude Console. You choose an [expiration](https://platform.claude.com/docs/en/manage-claude/authentication#key-expiration) as part of creation. Use [workspaces](https://platform.claude.com/settings/workspaces) to scope keys by project or environment. * **Send the key:** Set the `x-api-key` header on direct HTTP requests, or set the `ANTHROPIC_API_KEY` environment variable and the [client SDKs](https://platform.claude.com/docs/en/cli-sdks-libraries/overview) pick it up automatically. ```http POST /v1/messages x-api-key: YOUR_API_KEY anthropic-version: 2023-06-01 content-type: application/json ``` Store API keys in a secrets manager, rotate them periodically, and revoke any key you suspect has leaked. You can also set an [expiration](https://platform.claude.com/docs/en/manage-claude/authentication#key-expiration) when you create a key to limit how long a leaked credential stays usable. ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "messages": [{"role": "user", "content": "Hello, Claude"}] }' ``` ```python Python client = Anthropic(api_key="my-anthropic-api-key") # or, with ANTHROPIC_API_KEY set in the environment: client = Anthropic() ``` ```typescript TypeScript const client = new Anthropic({ apiKey: "my-anthropic-api-key" }); // or, with ANTHROPIC_API_KEY set in the environment: // const client = new Anthropic(); ``` ```go Go client := anthropic.NewClient( option.WithAPIKey("sk-ant-api03-..."), // defaults to os.LookupEnv("ANTHROPIC_API_KEY") ) ``` ```java Java import com.anthropic.client.AnthropicClient; import com.anthropic.client.okhttp.AnthropicOkHttpClient; // Explicit AnthropicClient client = AnthropicOkHttpClient.builder() .apiKey("my-anthropic-api-key") .build(); // From ANTHROPIC_API_KEY (or anthropic.apiKey system property) AnthropicClient clientFromEnv = AnthropicOkHttpClient.fromEnv(); ``` ```csharp C# using Anthropic; AnthropicClient client = new() { ApiKey = "my-anthropic-api-key" }; // Or, with ANTHROPIC_API_KEY set in the environment: // AnthropicClient client = new(); ``` ```php PHP // Reads ANTHROPIC_API_KEY from the environment $client = new Client(); // Or pass the key explicitly: $client = new Client(apiKey: 'my-anthropic-api-key'); ``` ```ruby Ruby anthropic = Anthropic::Client.new(api_key: "my-anthropic-api-key") # or, with ANTHROPIC_API_KEY set in the environment: anthropic = Anthropic::Client.new ``` ```bash CLI # See /docs/en/cli-sdks-libraries/cli/authentication#api-key for zsh, bash, and Windows variants export ANTHROPIC_API_KEY=sk-ant-api03-... ``` ### Key expiration When you create an API key from the [API keys page](https://platform.claude.com/settings/keys) in the Claude Console, you choose an expiration: a preset (3 hours, 1 day, 7 days, or 30 days), a custom duration, or **Never** for keys you store in a secrets manager and rotate yourself. If your organization has a maximum expiration policy, the Console limits presets and custom durations to the policy maximum, and **Never** is unavailable. Existing keys keep their current behavior; expiration is set at creation time and cannot be changed afterward. The same expiration choice applies when you [create an Admin API key](https://platform.claude.com/docs/en/manage-claude/admin-api-keys) in the Claude Console. Anthropic emails the key's creator as the expiration approaches: 7 days before expiration for keys created with a lifetime of at least 14 days, and 1 day before for keys with a lifetime of at least 7 days. Keys with shorter lifetimes expire without a warning email. After a key expires, requests made with it return a `401 authentication_error`. Create a new key to restore access; expired keys cannot be reactivated. The Console API keys table shows each key's expiration, and the Admin API reports each key's `expires_at` timestamp on the [List API Keys](https://platform.claude.com/docs/en/api/admin/api_keys/list) and [Retrieve API Key](https://platform.claude.com/docs/en/api/admin/api_keys/retrieve) endpoints, so you can audit and rotate keys before they expire. The field is `null` for keys without an expiration. Expiration limits the lifetime of a leaked credential, but it is not a substitute for secret hygiene. Regardless of expiration, store keys in a secrets manager and revoke any key you suspect has leaked. ## Workload Identity Federation Workload Identity Federation (WIF) lets a workload authenticate with a short-lived identity token issued by an identity provider (IdP) you already trust, such as AWS IAM, Google Cloud, or any standards-compliant OIDC issuer (such as GitHub Actions, Kubernetes service accounts, SPIFFE, Microsoft Entra ID, or Okta). The workload exchanges its IdP-issued JWT at `POST /v1/oauth/token` for a short-lived Claude API access token, and the SDK refreshes that token automatically before it expires. There is no `sk-ant-api...` string to mint, distribute, or rotate. Federation removes long-lived Claude API keys from your environment, which shrinks the blast radius of a leaked credential and lets you manage access with the same IdP controls you already use for cloud resources. It does not, on its own, guarantee end-to-end security: the trust chain is only as strong as your identity provider's configuration, and a long-lived secret one hop upstream (for example, a static cloud credential that can mint IdP tokens) can still undermine it. Pair federation with your provider's controls, such as IP allowlists, MFA, and audit logging. To configure federation, you create three resources in the Claude Console (a service account, a federation issuer, and a federation rule) and then point your SDK at the rule. See [Workload Identity Federation](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation) for the full setup walkthrough. ## App Attest App Attest authenticates iOS and macOS apps that call the Claude API directly from the device. Each installation proves that it is a genuine, unmodified build of an app you registered in the Claude Console, using Apple's App Attest service. Anthropic then issues the device a short-lived access token that bills usage to your workspace. Tokens are scoped to your workspace, expire after one hour, and authorize only [Messages API](https://platform.claude.com/docs/en/api/messages/create) calls. To register your app and get a client ID, see [App Attest for iOS and macOS apps](https://platform.claude.com/docs/en/manage-claude/app-attest). ## Next steps Configure issuers, rules, and service accounts, then exchange tokens Step-by-step guides for AWS, Google Cloud, Azure, GitHub Actions, Kubernetes, SPIFFE, and Okta Environment variables, validation rules, profile configuration, and error reference Let genuine installations of your app call the Claude API without shipping an API key Python, TypeScript, C#, Go, Java, PHP, Ruby, and the CLI --- title: Get your Claude API key url: https://platform.claude.com/docs/en/get-api-key description: Find, create, and manage your API keys for the Claude API in the Claude Console. --- API keys for the Claude API (also called Anthropic API keys) live in the Claude Console. To view your existing keys or create a new one, go to [Settings → API keys](https://platform.claude.com/settings/keys). ## Create an API key Go to [platform.claude.com](https://platform.claude.com/) and sign in, or create an account if you don't have one yet. Go to [Settings → API keys](https://platform.claude.com/settings/keys). Click **Create key**, then give the key a name. You can also choose a [workspace](https://platform.claude.com/settings/workspaces) to scope the key to, and an expiration. The Console shows the full key, which starts with `sk-ant-`, only once, at creation. Copy it and store it somewhere safe, such as a secrets manager. If you lose a key, you can't view it again in the Console. Create a new key instead. If the **Create key** button is disabled, you may not have permission to create keys in that workspace. Ask an organization admin to grant you access or to create a key for you. ## Use your API key Set the key as an environment variable: ```bash export ANTHROPIC_API_KEY="sk-ant-api03-..." ``` The [client SDKs](https://platform.claude.com/docs/en/cli-sdks-libraries/overview) read `ANTHROPIC_API_KEY` automatically. Direct HTTP requests send the key in the `x-api-key` header. To make your first request, follow the [Quickstart](https://platform.claude.com/docs/en/get-started), and see [Authentication](https://platform.claude.com/docs/en/manage-claude/authentication) for the full picture, including short-lived credentials with Workload Identity Federation. ## API keys and the Admin API The [Admin API](https://platform.claude.com/docs/en/api/admin) includes endpoints for managing your organization's API keys programmatically, such as [Retrieve API Key](https://platform.claude.com/docs/en/api/admin/api_keys/retrieve) and [List API Keys](https://platform.claude.com/docs/en/api/admin/api_keys/list). These endpoints are for organization admins automating key management. They require a separate [Admin API key](https://platform.claude.com/docs/en/manage-claude/admin-api-keys), and they never return a key's secret value, only a partially redacted hint. The Admin API can't recover a lost key or give you a key to call the Claude API with. To get a usable API key, create one in [Settings → API keys](https://platform.claude.com/settings/keys) in the Claude Console. ### Building with Claude --- title: Features overview url: https://platform.claude.com/docs/en/build-with-claude/overview description: Explore Claude's advanced features and capabilities. --- Claude's API surface is organized into five areas: * **Model capabilities:** Control how Claude reasons and formats responses. * **Tools:** Let Claude take actions on the web or in your environment. * **Tool infrastructure:** Handles discovery and orchestration at scale. * **Context management:** Keeps long-running sessions efficient. * **Files and assets:** Manage the documents and data you provide to Claude. If you're new, start with [model capabilities](https://platform.claude.com/docs/en/build-with-claude/overview#model-capabilities) and [tools](https://platform.claude.com/docs/en/build-with-claude/overview#tools). Return to the other sections when you're ready to optimize cost, latency, or scale. For administration and governance, see the [Admin API](https://platform.claude.com/docs/en/manage-claude/admin-api), the [Usage and Cost API](https://platform.claude.com/docs/en/manage-claude/usage-cost-api), and the [Compliance API](https://platform.claude.com/docs/en/manage-claude/compliance-api). ## Feature availability Features on the Claude Platform are assigned one of the following availability classifications per platform (shown in the Availability column of each following table). Not all features pass through every stage. A feature may enter at any classification and may skip stages. | Classification | Description | | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Beta** | Preview features used for gathering feedback and iterating on a less mature use case. Availability may be limited, including through sign-up requirements or waitlists, and may not be publicly announced. Features may change significantly or be discontinued based on feedback. Not guaranteed for ongoing production use. Breaking changes are possible with notice, and some platform-specific limitations may apply. Beta features on the Claude API and [Claude Platform on AWS](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws) have a [beta header](https://platform.claude.com/docs/en/api/beta-headers). | | **Generally available (GA)** | Feature is stable, fully supported, and recommended for production use. Should not have a beta header or other indicator that the feature is in a preview state. Covered by standard API [versioning](https://platform.claude.com/docs/en/api/versioning) guarantees. | | **Deprecated** | Feature is still functional but no longer recommended. A migration path and removal timeline are provided. | | **Retired** | Feature is no longer available. | **Platform labels:** Claude API (Anthropic first-party) · [Bedrock](https://platform.claude.com/docs/en/build-with-claude/claude-in-amazon-bedrock) (AWS-operated) · [Claude Platform on AWS](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws) (Anthropic-operated on AWS) · [Google Cloud](https://platform.claude.com/docs/en/build-with-claude/claude-on-vertex-ai) (Google-operated) · [Microsoft Foundry](https://platform.claude.com/docs/en/build-with-claude/claude-in-microsoft-foundry) (Anthropic-operated on Azure) ## Model capabilities Ways to steer Claude and Claude's direct outputs, including response format, reasoning depth, and input modalities. You can discover which capabilities a model supports programmatically. The [Models API](https://platform.claude.com/docs/en/api/models/list) returns `max_input_tokens`, `max_tokens`, and a `capabilities` object for every available model. The ZDR column indicates whether a feature is available under a Zero Data Retention arrangement. For most features this depends only on what the feature mechanism retains; for features tied to specific models, model-level ZDR availability also applies. See [Model-specific data retention requirements](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#model-specific-data-retention-requirements). | Feature | Description | Zero Data Retention (ZDR) | Availability | | --------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | [Context windows](https://platform.claude.com/docs/en/build-with-claude/context-windows) | Up to 1M tokens for processing large documents, extensive code bases, and long conversations. | ZDR eligible | | | [Adaptive thinking](https://platform.claude.com/docs/en/build-with-claude/thinking) | Let Claude dynamically decide when and how much to think. The only thinking mode on Claude 4.7 and later models. Use the effort parameter to control thinking depth. | ZDR eligible | | | [Batch processing](https://platform.claude.com/docs/en/build-with-claude/batch-processing) | Process large volumes of requests asynchronously for cost savings. Send batches with a large number of queries per batch. Batch API calls cost 50% less than standard API calls. | Not ZDR eligible | | | [Citations](https://platform.claude.com/docs/en/build-with-claude/citations) | Ground Claude's responses in source documents. With Citations, Claude can provide detailed references to the exact sentences and passages it uses to generate responses, leading to more verifiable, trustworthy outputs. | ZDR eligible | | | [Data residency](https://platform.claude.com/docs/en/manage-claude/data-residency) | Control where model inference runs using geographic controls. Specify `"global"` or `"us"` routing per request through the `inference_geo` parameter. | ZDR eligible | | | [Effort](https://platform.claude.com/docs/en/build-with-claude/effort) | Control how many tokens Claude uses when responding with the effort parameter, trading off between response thoroughness and token efficiency. | ZDR eligible | | | [Fallback credit](https://platform.claude.com/docs/en/build-with-claude/fallback-credit) | Avoid paying the prompt-cache cost twice when you retry a refused request on another model. The refusal carries a credit token, and echoing it on the retry bills the retry as though the conversation had been on the new model all along. Message Batches results do not include fallback credit tokens. | Not ZDR eligible\* | | | [PDF support](https://platform.claude.com/docs/en/build-with-claude/pdf-support) | Process and analyze text and visual content from PDF documents. | ZDR eligible | | | [Search results](https://platform.claude.com/docs/en/build-with-claude/search-results) | Enable natural citations for RAG applications by providing search results with proper source attribution. Achieve web search-quality citations for custom knowledge bases and tools. | ZDR eligible | | | [Server-side fallback](https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback) | Retry a refused request inside a single API call. Use the `"default"` mode to apply Anthropic's recommended fallback models, or name up to three models of your own; when the requested model declines, the API runs the next model in the chain on the same request. The `fallbacks` parameter is not available in the Message Batches API. | Not ZDR eligible\* | | | [Structured outputs](https://platform.claude.com/docs/en/build-with-claude/structured-outputs) | Guarantee schema conformance with two approaches: JSON outputs for structured data responses, and strict tool use for validated tool inputs. | [ZDR eligible (qualified)](https://platform.claude.com/docs/en/build-with-claude/structured-outputs#data-retention)\* | † | | [Thinking](https://platform.claude.com/docs/en/build-with-claude/thinking) | Enhanced reasoning capabilities for complex tasks, providing transparency into Claude's step-by-step thought process before delivering its final answer. | ZDR eligible | | ## Tools Built-in tools that Claude invokes through `tool_use`. Server-side tools are run by the platform; client-side tools are implemented and executed by you. ### Server-side tools | Feature | Description | ZDR | Availability | | --------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | ---------------------------------------------------------------------- | | [Advisor tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/advisor-tool) | Pair a faster executor model with a higher-intelligence advisor model that provides strategic guidance mid-generation for long-horizon agentic workloads. | ZDR eligible | | | [Code execution](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool) | Run code in a sandboxed environment for advanced data analysis, calculations, and file processing. Free when used with web search or web fetch. | Not ZDR eligible | † | | [Web fetch](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-fetch-tool) | Retrieve full content from specified web pages and PDF documents for in-depth analysis. | ZDR eligible\* | † | | [Web search](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-search-tool) | Augment Claude's comprehensive knowledge with current, real-world data from across the web. | ZDR eligible\* | † | ### Client-side tools | Feature | Description | ZDR | Availability | | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------ | ------------------------------------------------------------------------------------------------- | | [Bash](https://platform.claude.com/docs/en/agents-and-tools/tool-use/bash-tool) | Execute bash commands and scripts to interact with the system shell and perform command-line operations. | ZDR eligible | | | [Computer use](https://platform.claude.com/docs/en/agents-and-tools/tool-use/computer-use-tool) | Control computer interfaces by taking screenshots and issuing mouse and keyboard commands. | ZDR eligible | | | [Memory](https://platform.claude.com/docs/en/agents-and-tools/tool-use/memory-tool) | Enable Claude to store and retrieve information across conversations. Build knowledge bases over time, maintain project context, and learn from past interactions. | ZDR eligible | | | [Text editor](https://platform.claude.com/docs/en/agents-and-tools/tool-use/text-editor-tool) | Create and edit text files with a built-in text editor interface for file manipulation tasks. | ZDR eligible | | ## Tool infrastructure Infrastructure that supports discovering, orchestrating, and scaling tool use. | Feature | Description | ZDR | Availability | | ------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------ | | [Agent Skills](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview) | Extend Claude's capabilities with Skills. Use pre-built Skills (PowerPoint, Excel, Word, PDF) or create custom Skills with instructions and scripts. Skills use progressive disclosure to efficiently manage context. | Not ZDR eligible | † | | [Fine-grained tool streaming](https://platform.claude.com/docs/en/agents-and-tools/tool-use/fine-grained-tool-streaming) | Stream tool use parameters without buffering/JSON validation, reducing latency for receiving large parameters. | ZDR eligible | | | [MCP connector](https://platform.claude.com/docs/en/agents-and-tools/mcp-connector) | Connect to remote [MCP](https://platform.claude.com/docs/en/mcp) servers directly from the Messages API without a separate MCP client. | Not ZDR eligible | † | | [Programmatic tool calling](https://platform.claude.com/docs/en/agents-and-tools/tool-use/programmatic-tool-calling) | Enable Claude to call your tools programmatically from within code execution containers, reducing latency and token consumption for multi-tool workflows. | Not ZDR eligible | † | | [Tool search](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool) | Scale to thousands of tools by dynamically discovering and loading tools on-demand using regex- and BM25-based search, optimizing context usage and improving tool selection accuracy. | ZDR eligible | † | ## Context management Infrastructure for controlling and optimizing Claude's context window. | Feature | Description | ZDR | Availability | | ------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------- | | [Compaction](https://platform.claude.com/docs/en/build-with-claude/compaction) | Server-side context summarization for long-running conversations. When context approaches the window limit, the API automatically summarizes earlier parts of the conversation. | ZDR eligible | | | [Context editing](https://platform.claude.com/docs/en/build-with-claude/context-editing) | Automatically manage conversation context with configurable strategies. Supports clearing tool results when approaching token limits and managing thinking blocks in extended thinking conversations. | ZDR eligible | | | [Automatic prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#automatic-caching) | Simplify prompt caching to a single API parameter. The system automatically caches the last cacheable block in your request, moving the cache point forward as conversations grow. | ZDR eligible | | | [Prompt caching (5m)](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) | Provide Claude with more background knowledge and example outputs to reduce costs and latency. | ZDR eligible | | | [Prompt caching (1hr)](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#1-hour-cache-duration) | Extended 1-hour cache duration for less frequently accessed but important context, complementing the standard 5-minute cache. | ZDR eligible | | | [Token counting](https://platform.claude.com/docs/en/build-with-claude/token-counting) | Token counting enables you to determine the number of tokens in a message before sending it to Claude, helping you make informed decisions about your prompts and usage. | ZDR eligible | | ## Files and assets Manage files and assets for use with Claude. | Feature | Description | ZDR | Availability | | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- | ---------------- | ------------------------------------------------------------------------- | | [Files API](https://platform.claude.com/docs/en/build-with-claude/files) | Upload and manage files to use with Claude without re-uploading content with each request. Supports PDFs, images, and text files. | Not ZDR eligible | † | \* **Structured outputs:** Your prompts and Claude's outputs are not stored. Only JSON schemas are cached, for up to 24 hours since last use. **Web search and web fetch:** ZDR-eligible except when [dynamic filtering](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-search-tool#dynamic-filtering) is enabled. **Fallback credit and server-side fallback:** The features retain no message content, but both handle refusals from Claude Fable 5, which [is not available under ZDR](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#model-specific-data-retention-requirements). See [ZDR details](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#feature-eligibility). † On Microsoft Foundry, feature availability differs by [hosting option](https://platform.claude.com/docs/en/build-with-claude/claude-in-microsoft-foundry#hosting-options). These features are available on Hosted on Anthropic deployments, and not on Hosted on Azure deployments. --- title: Fallback credit url: https://platform.claude.com/docs/en/build-with-claude/fallback-credit description: Avoid paying the prompt-cache cost twice when you retry a refused Claude Fable 5 request on another model. --- Prompt caches are per-model. When Claude Fable 5 declines a request and you retry on another model, the conversation prefix that was already cached for Claude Fable 5 must be written into the new model's cache from scratch. Cache writes cost more than cache reads. Fallback credit removes that extra cost. The refusal carries a credit token, you echo the token on the retry, and the retry is billed as though the conversation had been on the new model all along. You need this page only when you build the retry yourself: over raw HTTP or with custom retry logic. [Server-side fallback](https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback#server-side-fallback) and the [SDK middleware](https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback#client-side-fallback) apply fallback credit automatically. If you use either, skip this page. [Refusals and fallback](https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback) covers detecting refusals and choosing a fallback approach. [Prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) explains cache reads and cache writes if those terms are new. ## The basic flow Send the request that may be refused with the `anthropic-beta: fallback-credit-2026-07-01` header. The `server-side-fallback-2026-07-01` header also grants the same fields, and the earlier `fallback-credit-2026-06-01` header remains accepted and grants the same fields. On a refusal, `stop_details` includes two fields: * **`fallback_credit_token`:** an opaque string that represents the credit. * **`fallback_has_prefill_claim`:** a Boolean that tells you which retry body shape to use. Both are `null` when no credit is available for the refusal. Start from the refused request body. Set `model` to the fallback model and add the token as the top-level `fallback_credit_token` parameter. Pick the body shape from the table below. Send the retry with the same `fallback-credit-2026-07-01` beta header. The retry needs the header to redeem the token. The `fallback_has_prefill_claim` field tells you whether the retry can continue the refused model's partial output instead of starting over: | `fallback_has_prefill_claim` | Retry body | | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `true` | The refused request body, unchanged, plus one appended assistant message whose `content` echoes the refused response's `content`. The retry model continues the response from where the refused model stopped, and completed server tool calls are not re-executed. | | `false` | The refused request body, unchanged. | ## Example The following example makes a request that may be refused and redeems the credit token on a retry against Claude Opus 4.8. When a retry attempt is rejected, the example degrades through the rejection ladder: the sequence of progressively simpler retry shapes covered in [When a retry is rejected](https://platform.claude.com/docs/en/build-with-claude/fallback-credit#when-a-retry-is-rejected). ```bash cURL # Initial request (may be refused) response=$(curl --fail-with-body -sS https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: fallback-credit-2026-07-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-fable-5", "max_tokens": 1024, "messages": [{"role": "user", "content": "Hello, Claude"}] }') # A refusal carries a one-time credit token in stop_details token=$(jq -r '.stop_details.fallback_credit_token // empty' <<<"${response}") if [[ -n "${token}" ]]; then # Retry on the fallback model with the credit token (same body) response=$(curl --fail-with-body -sS https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: fallback-credit-2026-07-01" \ -H "content-type: application/json" \ -d "$(jq -n --arg token "${token}" '{ model: "claude-opus-4-8", max_tokens: 1024, messages: [{"role": "user", "content": "Hello, Claude"}], fallback_credit_token: $token }')") fi # See the SDK examples for the full rejection-handling ladder. jq -c '{stop_reason, model}' <<<"${response}" ``` ```bash CLI # Initial request (may be refused) response=$(ant beta:messages create \ --model claude-fable-5 \ --max-tokens 1024 \ --message '{"role":"user","content":"Hello, Claude"}' \ --beta fallback-credit-2026-07-01 \ --format json) # A refusal carries a one-time credit token in stop_details token=$(jq -r '.stop_details.fallback_credit_token // empty' <<<"${response}") if [[ -n "${token}" ]]; then # Retry on the fallback model with the credit token response=$(ant beta:messages create \ --model claude-opus-4-8 \ --max-tokens 1024 \ --message '{"role":"user","content":"Hello, Claude"}' \ --fallback-credit-token "${token}" \ --beta fallback-credit-2026-07-01 \ --format json) fi # See the SDK examples for the full rejection-handling ladder. jq -c '{stop_reason, model}' <<<"${response}" ``` ```python Python client = Anthropic() request = { "max_tokens": 1024, "messages": [{"role": "user", "content": "Hello, Claude"}], } def send(model: str, body: dict[str, object]) -> BetaMessage: return client.beta.messages.create( model=model, betas=["fallback-credit-2026-07-01"], **body ) response = send("claude-fable-5", request) if ( response.stop_reason == "refusal" and (details := response.stop_details) and (token := details.fallback_credit_token) ): exact_body = request | {"fallback_credit_token": token} # Prefer the continuation shape unless the claim is False if details.fallback_has_prefill_claim is not False: echoed = [block.model_dump() for block in response.content] match echoed: case [*_, {"type": "text"} as final_block]: final_block["text"] = final_block["text"].rstrip() attempt = exact_body | { "messages": [ *request["messages"], {"role": "assistant", "content": echoed}, ] } else: attempt = exact_body try: response = send("claude-opus-4-8", attempt) except BadRequestError as error: if "redemption temporarily unavailable" in error.message: raise # Transient: retry with the token within its five-minute window try: # Fall back to the unchanged body, still with the token response = send("claude-opus-4-8", exact_body) except BadRequestError as retry_error: if "redemption temporarily unavailable" in retry_error.message: raise # Transient: retry with the token within its five-minute window # The token itself was rejected: forfeit it and retry without. response = send("claude-opus-4-8", request) print(json.dumps({"stop_reason": response.stop_reason, "model": response.model})) ``` ```typescript TypeScript const client = new Anthropic(); const request: Anthropic.Beta.MessageCreateParamsNonStreaming = { model: "claude-fable-5", max_tokens: 1024, messages: [{ role: "user", content: "Hello, Claude" }], betas: ["fallback-credit-2026-07-01"] }; let response = await client.beta.messages.create(request); if ( response.stop_reason === "refusal" && response.stop_details?.type === "refusal" && response.stop_details.fallback_credit_token ) { const { fallback_credit_token, fallback_has_prefill_claim } = response.stop_details; const fallbackModel = "claude-opus-4-8"; const exactRetry: Anthropic.Beta.MessageCreateParamsNonStreaming = { ...request, model: fallbackModel, fallback_credit_token }; // Richest shape first, degrading on each rejection: the continuation // shape (unless the claim is false), the unchanged body still carrying // the token, and finally forfeiting the token. let attempt = exactRetry; if (fallback_has_prefill_claim !== false) { const finalBlock = response.content.at(-1); const echoed: Anthropic.Beta.BetaContentBlockParam[] = finalBlock?.type === "text" ? [ ...response.content.slice(0, -1), { ...finalBlock, text: finalBlock.text.trimEnd() } ] : response.content; attempt = { ...exactRetry, messages: [...request.messages, { role: "assistant", content: echoed }] }; } try { response = await client.beta.messages.create(attempt); } catch (error) { // Degrade only on a shape-related 400. "redemption temporarily // unavailable" is transient: retry the same way within the token's // five-minute window instead. if ( !(error instanceof Anthropic.BadRequestError) || error.message.includes("redemption temporarily unavailable") ) { throw error; } try { response = await client.beta.messages.create(exactRetry); } catch (retryError) { if ( !(retryError instanceof Anthropic.BadRequestError) || retryError.message.includes("redemption temporarily unavailable") ) { throw retryError; } response = await client.beta.messages.create({ ...request, model: fallbackModel }); } } } const { stop_reason, model } = response; console.log(JSON.stringify({ stop_reason, model })); ``` ```csharp C# var client = new AnthropicClient(); const string beta = "fallback-credit-2026-07-01"; List requestMessages = [ new() { Role = Role.User, Content = "Hello, Claude" }, ]; MessageCreateParams Request(string model) => new() { Model = model, MaxTokens = 1024, Messages = requestMessages, Betas = [beta], }; var response = await client.Beta.Messages.Create(Request("claude-fable-5")); if ( response.StopReason == BetaStopReason.Refusal && response.StopDetails is { FallbackCreditToken: string token } details ) { var exactBody = Request("claude-opus-4-8") with { FallbackCreditToken = token }; var attempt = exactBody; // Prefer the continuation shape unless the claim is false if (details.FallbackHasPrefillClaim is not false) { var echoed = JsonArray.Create(response.RawData["content"])!; if ( echoed is [.., JsonObject lastBlock] && lastBlock["type"]?.GetValue() is "text" && lastBlock["text"]?.GetValue() is string text ) { lastBlock["text"] = text.TrimEnd(); } attempt = exactBody with { Messages = [ .. requestMessages, new() { Role = Role.Assistant, Content = new BetaMessageParamContent( JsonSerializer.SerializeToElement(echoed) ), }, ], }; } // A transient "redemption temporarily unavailable" rejection propagates out of // each of the following catch filters: retry with the token within its five-minute window. try { response = await client.Beta.Messages.Create(attempt); } catch (AnthropicBadRequestException e) when (!e.Message.Contains("redemption temporarily unavailable")) { try { // Fall back to the unchanged body, still with the token response = await client.Beta.Messages.Create(exactBody); } catch (AnthropicBadRequestException retryError) when (!retryError.Message.Contains("redemption temporarily unavailable")) { // The token itself was rejected: forfeit it and retry without. response = await client.Beta.Messages.Create(Request("claude-opus-4-8")); } } } Console.WriteLine( JsonSerializer.Serialize( new { stop_reason = response.StopReason?.Raw(), model = response.Model.Raw() } ) ); ``` ```go Go ctx := context.Background() client := anthropic.NewClient() request := anthropic.BetaMessageNewParams{ MaxTokens: 1024, Betas: []anthropic.AnthropicBeta{anthropic.AnthropicBetaFallbackCredit2026_07_01}, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Hello, Claude")), }, } send := func(model anthropic.Model, body anthropic.BetaMessageNewParams) (*anthropic.BetaMessage, error) { body.Model = model return client.Beta.Messages.New(ctx, body) } // A non-transient 400 means this attempt shape or token was rejected and // the next rung of the ladder should run. "redemption temporarily // unavailable" is transient: surface it and retry with the token within // its five-minute window. canFallBack := func(err error) bool { apiErr, ok := errors.AsType[*anthropic.Error](err) return ok && apiErr.StatusCode == 400 && !strings.Contains(apiErr.Error(), "redemption temporarily unavailable") } response, err := send(anthropic.ModelClaudeFable5, request) if err != nil { log.Fatal(err) } if response.StopReason == anthropic.BetaStopReasonRefusal { details := response.StopDetails if token := details.FallbackCreditToken; token != "" { exactBody := request exactBody.FallbackCreditToken = anthropic.BetaMessageNewParamsFallbackCreditTokenUnion{ OfString: anthropic.String(token), } attempt := exactBody // Prefer the continuation shape unless the claim is false if details.FallbackHasPrefillClaim || !details.JSON.FallbackHasPrefillClaim.Valid() { echoed := response.ToParam() if len(echoed.Content) > 0 { if text := echoed.Content[len(echoed.Content)-1].OfText; text != nil { text.Text = strings.TrimRightFunc(text.Text, unicode.IsSpace) } } attempt.Messages = append(slices.Clone(request.Messages), echoed) } response, err = send(anthropic.ModelClaudeOpus4_8, attempt) if err != nil && canFallBack(err) { // Fall back to the unchanged body, still with the token response, err = send(anthropic.ModelClaudeOpus4_8, exactBody) if err != nil && canFallBack(err) { // The token itself was rejected: forfeit it and retry without. response, err = send(anthropic.ModelClaudeOpus4_8, request) } } if err != nil { log.Fatal(err) } } } summary, err := json.Marshal(struct { StopReason anthropic.BetaStopReason `json:"stop_reason"` Model anthropic.Model `json:"model"` }{response.StopReason, response.Model}) if err != nil { log.Fatal(err) } fmt.Println(string(summary)) ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams.Builder request() { return MessageCreateParams.builder() .maxTokens(1024L) .addUserMessage("Hello, Claude") .addBeta(AnthropicBeta.FALLBACK_CREDIT_2026_07_01); } BetaMessage send(Model model, MessageCreateParams.Builder body) { return client.beta().messages().create(body.model(model).build()); } void main() { BetaMessage response = send(Model.CLAUDE_FABLE_5, request()); if (response.stopReason().map(BetaStopReason.REFUSAL::equals).orElse(false) && response.stopDetails().orElse(null) instanceof BetaRefusalStopDetails details && details.fallbackCreditToken().orElse(null) instanceof String creditToken) { MessageCreateParams.Builder attempt = request().fallbackCreditToken(creditToken); // Prefer the continuation shape unless the claim is false if (details.fallbackHasPrefillClaim().orElse(true)) { List echoed = new ArrayList<>( response.content().stream().map(BetaContentBlock::toParam).toList()); if (!echoed.isEmpty() && echoed.getLast().isText()) { var lastText = echoed.removeLast().asText(); echoed.addLast(BetaContentBlockParam.ofText( lastText.toBuilder().text(lastText.text().stripTrailing()).build())); } attempt.addAssistantMessageOfBetaContentBlockParams(echoed); } try { response = send(Model.CLAUDE_OPUS_4_8, attempt); } catch (BadRequestException badRequest) { // Transient: retry with the token within its five-minute window if (badRequest.getMessage().contains("redemption temporarily unavailable")) { throw badRequest; } try { // Fall back to the unchanged body, still with the token response = send(Model.CLAUDE_OPUS_4_8, request().fallbackCreditToken(creditToken)); } catch (BadRequestException retryBadRequest) { if (retryBadRequest.getMessage().contains("redemption temporarily unavailable")) { throw retryBadRequest; } // The token itself was rejected: forfeit it and retry without. response = send(Model.CLAUDE_OPUS_4_8, request()); } } } IO.println(""" {"stop_reason": "%s", "model": "%s"}""" .formatted(response.stopReason().orElseThrow(), response.model())); } ``` ```php PHP $client = new Client(); $beta = 'fallback-credit-2026-07-01'; $messages = [['role' => 'user', 'content' => 'Hello, Claude']]; $send = fn (string $model, array $messages, ?string $token = null) => $client->beta->messages->create( maxTokens: 1024, messages: $messages, model: $model, fallbackCreditToken: $token, betas: [$beta], ); $response = $send('claude-fable-5', $messages); $token = $response->stopReason === 'refusal' ? $response->stopDetails?->fallbackCreditToken : null; if ($token !== null) { $attemptMessages = $messages; // Prefer the continuation shape unless the claim is false if ($response->stopDetails->fallbackHasPrefillClaim !== false) { $echoed = $response->content |> json_encode(...) |> (fn (string $json): array => json_decode($json, associative: true)); $lastIndex = array_key_last($echoed); if ($lastIndex !== null && $echoed[$lastIndex]['type'] === 'text') { $echoed[$lastIndex]['text'] = rtrim($echoed[$lastIndex]['text']); } $attemptMessages[] = ['role' => 'assistant', 'content' => $echoed]; } // Transient: retry with the token within its five-minute window $isTransientRedemption = fn (BadRequestException $error): bool => str_contains($error->getMessage(), 'redemption temporarily unavailable'); try { $response = $send('claude-opus-4-8', $attemptMessages, $token); } catch (BadRequestException $error) { if ($isTransientRedemption($error)) { throw $error; } try { // Fall back to the unchanged body, still with the token $response = $send('claude-opus-4-8', $messages, $token); } catch (BadRequestException $retryError) { if ($isTransientRedemption($retryError)) { throw $retryError; } // The token itself was rejected: forfeit it and retry without. $response = $send('claude-opus-4-8', $messages); } } } echo json_encode(['stop_reason' => $response->stopReason, 'model' => $response->model]), PHP_EOL; ``` ```ruby Ruby client = Anthropic::Client.new request = { max_tokens: 1024, messages: [{role: "user", content: "Hello, Claude"}] } send_message = ->(model, body) do client.beta.messages.create(model:, betas: ["fallback-credit-2026-07-01"], **body) end response = send_message.call("claude-fable-5", request) if response in {stop_reason: :refusal, stop_details: {fallback_credit_token: String => credit_token} => details} exact_body = request.merge(fallback_credit_token: credit_token) # Prefer the continuation shape unless the claim is false attempt = if details.fallback_has_prefill_claim != false echoed = response.content.map(&:to_h) if echoed.last in {type: :text, text: String => final_text} echoed[-1] = echoed.last.merge(text: final_text.rstrip) end exact_body.merge( messages: [*request[:messages], {role: "assistant", content: echoed}] ) else exact_body end begin response = send_message.call("claude-opus-4-8", attempt) rescue Anthropic::Errors::BadRequestError => error # Transient: retry with the token within its five-minute window raise if error.message.include?("redemption temporarily unavailable") begin # Fall back to the unchanged body, still with the token response = send_message.call("claude-opus-4-8", exact_body) rescue Anthropic::Errors::BadRequestError => error # Transient: retry with the token within its five-minute window raise if error.message.include?("redemption temporarily unavailable") # The token itself was rejected: forfeit it and retry without. response = send_message.call("claude-opus-4-8", request) end end end puts JSON.generate({stop_reason: response.stop_reason, model: response.model}) ``` ## Where it works Fallback credit is in beta on the Claude API, Amazon Bedrock, Claude Platform on AWS, Google Cloud, and Microsoft Foundry. Refusals in [Message Batches](https://platform.claude.com/docs/en/build-with-claude/batch-processing) don't mint credit tokens, and redemption applies only to direct Messages API requests: a token passed on a batch request is accepted but ignored. The retry model must be one of the refused model's permitted fallback targets. Claude Fable 5's permitted targets are Claude Opus 4.8 (`claude-opus-4-8`) and Claude Opus 5 (`claude-opus-5`). On the Claude API and Claude Platform on AWS, the target list is published as `allowed_fallback_models` on each model's entry in the [Models API](https://platform.claude.com/docs/en/api/models/list) when the `server-side-fallback-2026-07-01` beta header is set. The list is not yet visible under the `fallback-credit-*` header alone. It is not exposed on Amazon Bedrock, Google Cloud, or Microsoft Foundry. ## Checking that the credit applied The refund is visible in the retry's `usage`. Compared with what the same request would report without the token, `cache_creation_input_tokens` is lower, and `cache_read_input_tokens` is higher by the same amount. A shift of zero means the token was honored but there was nothing to reprice, for example because the retry model's cache was already warm. ## When a retry is rejected Most retries redeem on the first attempt. When one does not, the API returns a 400 error that tells you what to try next. If the retry that appends the assistant message is rejected with a 400 error, resend the refused request body unchanged, still with the token. If the unchanged body is also rejected with a 400 error whose message names `fallback_credit_token`, retry without the token. The credit is forfeited, but the retry itself goes through. If the refused request executed server tools, a tokenless retry re-runs and re-bills those tools. In that case, surface the 400 error to your caller instead of falling through to a tokenless retry. This rejection is transient, not a verdict on your retry shape. Retry the same request, with the same token, within the token's five-minute window. Do not move to the next step of the ladder. ## Reference The sections below cover edge cases and the complete redemption rules. Most integrations do not need them. Redemption compares the retry against the refused request. Every field that shapes the prompt must match exactly. Fields that do not shape the prompt may change on the retry. | Rule | Fields | | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Must match exactly | `system`, `messages`, `tools`, `tool_choice`, `thinking`, and `cache_control`, plus `output_config`, `mcp_servers`, `context_management`, and `container` when you use them | | May change on the retry | `model`, `max_tokens`, `stop_sequences`, `temperature`, `top_p`, `top_k`, `stream`, `metadata`, and `service_tier` | The continuation shape (`fallback_has_prefill_claim: true`) is the one exception to the `messages` match: it adds exactly one assistant message at the end of `messages`. Do not strip `thinking` or `redacted_thinking` blocks from earlier turns on the retry, even though a plain retry without a token usually strips them. The body must match the refused request, and the server handles those blocks itself. Send the same `anthropic-beta` headers on the retry as on the refused request. A beta header present on one of the two requests but not the other can fail the match even when the bodies are identical. The resulting 400 error carries the same `request body ... does not match` message as a body difference, so a header difference is easy to misread as a body problem. In particular, do not add or drop beta headers based on which model the request targets. Two header families are exempt from the match, for the retry's sake: * **`server-side-fallback-*`:** a retry must drop the `fallbacks` parameter, and dropping this header along with it does not cause a mismatch. * **`fallback-credit-*`:** keep this header on both requests. The retry needs it to redeem the token. On models that include the 1M token context window by default, such as Claude Fable 5, Claude Opus 5, and Claude Opus 4.8, the `context-1m-2025-08-07` beta header has no effect. The most robust way to keep the two requests identical is to omit that header on both, rather than sending it on one request and not the other. The field is `null` only when the token is also `null`, so a value you observe while holding a token is never `null`. It can still surface as absent (`None` in the typed SDKs) on Amazon Bedrock, Google Cloud, and Microsoft Foundry while their support for the field rolls out. In that case, treat the retry shape as unknown rather than as `false`. Try the appended-assistant-message shape first, and rely on the rejection handling in [When a retry is rejected](https://platform.claude.com/docs/en/build-with-claude/fallback-credit#when-a-retry-is-rejected), which falls back to the unchanged body. When a refusal's token supports the continuation shape, the response `content` carries only the model's own output, and the refusal explanation is delivered in `stop_details.explanation`. You can therefore echo `content` into the appended assistant message as-is. Two adjustments may still be needed before sending: * If the final block you send is a `text` block, strip its trailing whitespace. * Omit any client-side `tool_use` block that has no matching `tool_result`. If the echoed content includes a `fallback` block from an earlier [server-side fallback](https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback#server-side-fallback), keep the block exactly where it appeared. It is accepted on any request without a beta header. The API uses its position to validate the thinking blocks around it, so a request that echoes thinking blocks from both sides of that boundary is rejected if the block is omitted or moved. The token redeems only from the organization and workspace that received the refusal, including on Microsoft Foundry. On Amazon Bedrock and Google Cloud, which do not have workspaces, the token is bound to the platform's caller identity instead. The token expires five minutes after the refusal. After that, send the retry without it. The token is also stateless: the server stores nothing about it, and there is no endpoint to inspect or revoke it. When the refusal arrived after server tools had already executed within the request, the token redeems only by continuing the partial response. That restriction is what prevents the completed tool calls from running, and billing, again. One combination can therefore leave the token unredeemable by either shape, when both of the following are true: * The request used `output_config.format` or a `tool_choice` that forces tool use. Either one rules out the appended-assistant-message shape. * The refusal arrived after server tools had executed. That rules out the unchanged body. If the unchanged-body retry is rejected with a 400 error saying the token must be redeemed by continuing the partial response, discard the token. A retry without it goes through, but it re-runs and re-bills the completed server tools. Surface the cost or the error to your caller rather than retrying silently. ## Next steps Detect refusals and choose between server-side fallback, the SDK middleware, and a manual retry. How cache reads and cache writes are billed. Every `stop_reason` value and how to handle it. The SDK helper that applies fallback credit automatically. --- title: Refusals and fallback url: https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback description: How Claude Fable 5 and Claude Opus 5 return classifier refusals and how to retry refused requests on a fallback model. --- Claude Fable 5 and Claude Opus 5 include safety classifiers that can decline a request. When that happens, you receive a normal response, not an error, with `stop_reason: "refusal"`. You can usually still get an answer by sending the same request to another Claude model. This page shows you how to recognize a refusal and how to set up that retry. Read this page when you build on Claude Fable 5 or Claude Opus 5 and want declined requests to fall through to another model automatically. It also applies when you have just seen `"refusal"` in a response and want to know what to do next. Related pages: * [Stop reasons and fallback](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons): the full list of `stop_reason` values. * [Fallback credit](https://platform.claude.com/docs/en/build-with-claude/fallback-credit): how refused requests are billed, and how to avoid paying twice for prompt caching on a retry. * [SDK middleware](https://platform.claude.com/docs/en/cli-sdks-libraries/middleware): the SDK helper that wraps all of this. * [Fallback and billing cookbook](https://platform.claude.com/cookbook/fable-5-fallback-billing-guide): a worked end-to-end example. The simplest setup, in beta on the Claude API: set `fallbacks` to `"default"`, and the API retries a declined request on the fallback model Anthropic recommends for its refusal category. For categories with no recommended fallback, the refusal stands. ```bash cURL curl --fail-with-body -sS https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: server-side-fallback-2026-07-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-fable-5", "max_tokens": 1024, "fallbacks": "default", "messages": [{"role": "user", "content": "Hello, Claude"}] }' | jq -r '.model' ``` ```bash CLI ant beta:messages create \ --model claude-fable-5 \ --max-tokens 1024 \ --message '{"role":"user","content":"Hello, Claude"}' \ --fallbacks default \ --beta server-side-fallback-2026-07-01 \ --transform model --raw-output ``` ```python Python client = Anthropic() response = client.beta.messages.create( model="claude-fable-5", max_tokens=1024, messages=[{"role": "user", "content": "Hello, Claude"}], fallbacks="default", betas=["server-side-fallback-2026-07-01"], ) print(response.model) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.beta.messages.create({ model: "claude-fable-5", max_tokens: 1024, messages: [{ role: "user", content: "Hello, Claude" }], fallbacks: "default", betas: ["server-side-fallback-2026-07-01"] }); console.log(response.model); ``` ```csharp C# AnthropicClient client = new(); BetaMessage response = await client.Beta.Messages.Create( new() { Model = Messages::Model.ClaudeFable5, MaxTokens = 1024, Messages = [new() { Content = "Hello, Claude", Role = Role.User }], Fallbacks = new Default(), Betas = [AnthropicBeta.ServerSideFallback2026_07_01], } ); Console.WriteLine(response.Model.Raw()); ``` ```go Go client := anthropic.NewClient() response, err := client.Beta.Messages.New(context.Background(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeFable5, MaxTokens: 1024, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Hello, Claude")), }, Fallbacks: anthropic.BetaFallbacksParamOfDefault(), Betas: []anthropic.AnthropicBeta{anthropic.AnthropicBetaServerSideFallback2026_07_01}, }) if err != nil { panic(err) } fmt.Println(response.Model) ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); BetaMessage response = client.beta().messages().create(MessageCreateParams.builder() .model(Model.CLAUDE_FABLE_5) .maxTokens(1024L) .addUserMessage("Hello, Claude") .fallbacksDefault() .addBeta(AnthropicBeta.SERVER_SIDE_FALLBACK_2026_07_01) .build()); IO.println(response.model().asString()); ``` ```php PHP $client = new Client(); $response = $client->beta->messages->create( model: 'claude-fable-5', maxTokens: 1024, messages: [['role' => 'user', 'content' => 'Hello, Claude']], fallbacks: 'default', betas: ['server-side-fallback-2026-07-01'], ); echo $response->model, PHP_EOL; ``` ```ruby Ruby client = Anthropic::Client.new response = client.beta.messages.create( model: "claude-fable-5", max_tokens: 1024, messages: [{role: "user", content: "Hello, Claude"}], fallbacks: :default, betas: ["server-side-fallback-2026-07-01"] ) puts response.model ``` The following sections cover what a refusal response contains, when to use server-side or client-side fallback, and how each is billed. ## What a refusal looks like A refusal is a successful HTTP 200 response with `stop_reason: "refusal"`: ```json { "id": "msg_01XFUDYJgAACzvnptvVoYEL", "type": "message", "role": "assistant", "model": "claude-fable-5", "content": [], "stop_reason": "refusal", "stop_details": { "type": "refusal", "category": "cyber", "explanation": "This request was declined because it could enable cyber harm." }, "usage": { "input_tokens": 412, "output_tokens": 0 } } ``` The `stop_details` object explains the decline: * **`category`:** names the policy area that triggered the classifier. * **`explanation`:** a human-readable description. The text is not stable, so display it rather than parse it. * Both fields are `null` when the refusal does not map to a named category. That `null` is a normal, permanent value, not a placeholder. * `stop_details` itself is `null` for every stop reason other than `refusal`. | `category` | What it means | | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `"cyber"` | The request could enable cyber harm, such as malware or exploit development. Benign cybersecurity work can also trigger this category. | | `"bio"` | The request could enable biological harm, such as dangerous lab methods. Beneficial life sciences work can also trigger this category. | | `"frontier_llm"` | The request could assist the development of competing AI models, which is restricted under [Anthropic's commercial terms](https://www.anthropic.com/legal/commercial-terms). Benign machine learning work can also trigger this category. | | `"reasoning_extraction"` | The request asks the model to reproduce its internal reasoning in the response text. To get reasoning in a structured form instead, use [adaptive thinking](https://platform.claude.com/docs/en/build-with-claude/thinking). | | `"general_harms"` | The request could be related to an area that was determined as harmful. Benign work might sometimes trigger this category. | A refusal can arrive before any output, or mid-stream after partial output. In either case, treat any partial output as incomplete and discard it. **How refusals are billed:** You are not billed for a refusal that arrives before any output. `content` is empty, and token counts appear in `usage` but are not charged. The request still counts against your rate limits. A mid-stream refusal bills the input tokens and the output already streamed at normal rates. ## Picking a fallback approach There are three ways to retry a refused request on another model. The right one depends on where you are running and how much control you need. | Your situation | Use | Why | | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------- | | Claude API, simplest setup | [Server-side fallback](https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback#server-side-fallback) | One request, one response. The API handles the retry. | | Any platform, using an Anthropic SDK | [The SDK middleware](https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback#client-side-fallback) | Configure once on the client. Retries happen automatically. | | Raw HTTP or custom retry logic | Manual retry with [fallback credit](https://platform.claude.com/docs/en/build-with-claude/fallback-credit) | Full control. Fallback credit keeps the cost down. | Server-side fallback and the SDK middleware apply fallback credit for you. You only need the [Fallback credit](https://platform.claude.com/docs/en/build-with-claude/fallback-credit) page when you build the retry yourself. ## Server-side fallback Server-side fallback retries a refused request inside a single API call. In the default mode, when the primary model declines and the refusal category has a recommended fallback, the API runs the same request on the model Anthropic recommends for that category. You can instead name up to three fallback models of your own (below). Either way, you get back one response that names the model that answered, so your user gets an answer in one round trip. Server-side fallback is in beta on the Claude API. The `fallbacks` parameter is not supported on the [Message Batches API](https://platform.claude.com/docs/en/build-with-claude/batch-processing) (a batch item that includes it comes back as an errored result) and is not available on Amazon Bedrock, Google Cloud, or Microsoft Foundry. On those platforms, use [client-side fallback with the SDK middleware](https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback#client-side-fallback) instead. ### Making the request Set the `fallbacks` parameter to the string `"default"` and send the `server-side-fallback-2026-07-01` beta header. The API then applies the requested model's server-defined default routing, which selects a recommended fallback model based on the refusal category the classifier reports, so refused requests are served without you maintaining a model list as recommendations change. ```bash cURL curl --fail-with-body -sS https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: server-side-fallback-2026-07-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-fable-5", "max_tokens": 1024, "fallbacks": "default", "messages": [{"role": "user", "content": "Hello, Claude"}] }' | jq -c '{ stop_reason, model, # A fallback_message entry in usage.iterations means a fallback model ran; # pair it with stop_reason to confirm the fallback served the response. served_by_fallback: ( any(.usage.iterations[]?; .type == "fallback_message") and .stop_reason != "refusal" ) }' ``` ```bash CLI ant beta:messages create \ --model claude-fable-5 \ --max-tokens 1024 \ --message '{"role":"user","content":"Hello, Claude"}' \ --fallbacks default \ --beta server-side-fallback-2026-07-01 \ --format json | jq -c '{ stop_reason, model, # A fallback_message entry in usage.iterations means a fallback model ran; # pair it with stop_reason to confirm the fallback served the response. served_by_fallback: ( any(.usage.iterations[]?; .type == "fallback_message") and .stop_reason != "refusal" ) }' ``` ```python Python client = Anthropic() response = client.beta.messages.create( model="claude-fable-5", max_tokens=1024, messages=[{"role": "user", "content": "Hello, Claude"}], fallbacks="default", betas=["server-side-fallback-2026-07-01"], ) # A fallback_message entry in usage.iterations means a fallback model ran; # pair it with stop_reason to confirm the fallback served the response. fallback_ran = any( iteration.type == "fallback_message" for iteration in response.usage.iterations or [] ) served_by_fallback = fallback_ran and response.stop_reason != "refusal" print( json.dumps( { "stop_reason": response.stop_reason, "model": response.model, "served_by_fallback": served_by_fallback, } ) ) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.beta.messages.create({ model: "claude-fable-5", max_tokens: 1024, messages: [{ role: "user", content: "Hello, Claude" }], fallbacks: "default", betas: ["server-side-fallback-2026-07-01"] }); // A fallback_message entry in usage.iterations means a fallback model ran; // pair it with stop_reason to confirm the fallback served the response. const { stop_reason, model, usage } = response; const servedByFallback = (usage.iterations ?? []).some((entry) => entry.type === "fallback_message") && stop_reason !== "refusal"; console.log( JSON.stringify({ stop_reason, model, served_by_fallback: servedByFallback }) ); ``` ```csharp C# AnthropicClient client = new(); var response = await client.Beta.Messages.Create( new() { Model = Messages::Model.ClaudeFable5, MaxTokens = 1024, Messages = [ new() { Content = "Hello, Claude", Role = Role.User }, ], Fallbacks = new Default(), Betas = [AnthropicBeta.ServerSideFallback2026_07_01], } ); // A fallback_message entry in usage.iterations means a fallback model ran; // pair it with stop_reason to confirm the fallback served the response. bool fallbackRan = (response.Usage.Iterations ?? []).Any(iteration => iteration.TryPickBetaFallbackMessageIterationUsage(out _) ); bool servedByFallback = fallbackRan && response.StopReason?.Value() != BetaStopReason.Refusal; Console.WriteLine( JsonSerializer.Serialize( new { stop_reason = response.StopReason?.Raw(), model = response.Model.Raw(), served_by_fallback = servedByFallback, } ) ); ``` ```go Go client := anthropic.NewClient() response, err := client.Beta.Messages.New(context.Background(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeFable5, MaxTokens: 1024, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Hello, Claude")), }, Fallbacks: anthropic.BetaFallbacksParamOfDefault(), Betas: []anthropic.AnthropicBeta{anthropic.AnthropicBetaServerSideFallback2026_07_01}, }) if err != nil { panic(err) } // A fallback_message entry in usage.iterations means a fallback model ran; // pair it with stop_reason to confirm the fallback served the response. fallbackRan := slices.ContainsFunc( response.Usage.Iterations, func(iteration anthropic.BetaIterationsUsageItemUnion) bool { _, isFallback := iteration.AsAny().(anthropic.BetaFallbackMessageIterationUsage) return isFallback }, ) servedByFallback := fallbackRan && response.StopReason != anthropic.BetaStopReasonRefusal summary, err := json.Marshal(struct { StopReason anthropic.BetaStopReason `json:"stop_reason"` Model anthropic.Model `json:"model"` ServedByFallback bool `json:"served_by_fallback"` }{response.StopReason, response.Model, servedByFallback}) if err != nil { panic(err) } fmt.Println(string(summary)) ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); BetaMessage response = client.beta().messages().create( MessageCreateParams.builder() .model(Model.CLAUDE_FABLE_5) .maxTokens(1024L) .addUserMessage("Hello, Claude") .fallbacksDefault() .addBeta(AnthropicBeta.SERVER_SIDE_FALLBACK_2026_07_01) .build() ); // A fallback_message usage entry means a fallback model produced the // response; a refusal stop reason means no model served it. List iterations = response.usage().iterations().orElse(List.of()); boolean servedByFallback = iterations.stream().anyMatch(BetaUsage.Iteration::isFallbackMessage) && response.stopReason().filter(BetaStopReason.REFUSAL::equals).isEmpty(); IO.println(""" {"stop_reason":"%s","model":"%s","served_by_fallback":%b}\ """.formatted( response.stopReason().map(BetaStopReason::asString).orElse("null"), response.model().asString(), servedByFallback)); ``` ```php PHP $client = new Client(); $response = $client->beta->messages->create( maxTokens: 1024, messages: [['role' => 'user', 'content' => 'Hello, Claude']], model: 'claude-fable-5', fallbacks: 'default', betas: ['server-side-fallback-2026-07-01'], ); // A fallback_message entry in usage.iterations means a fallback model ran; // pair it with stop_reason to confirm the fallback served the response. $iterations = $response->usage->iterations ?? []; $servedByFallback = array_any($iterations, fn($entry) => $entry->type === 'fallback_message') && $response->stopReason !== 'refusal'; echo json_encode([ 'stop_reason' => $response->stopReason, 'model' => $response->model, 'served_by_fallback' => $servedByFallback, ]), PHP_EOL; ``` ```ruby Ruby client = Anthropic::Client.new response = client.beta.messages.create( model: "claude-fable-5", max_tokens: 1024, messages: [{role: "user", content: "Hello, Claude"}], fallbacks: :default, betas: ["server-side-fallback-2026-07-01"] ) # A fallback_message entry in usage.iterations means a fallback model ran; # pair it with stop_reason to confirm the fallback served the response. iterations = response.usage.iterations || [] served_by_fallback = iterations.any? { it.type == :fallback_message } && response.stop_reason != :refusal stop_reason = response.stop_reason model = response.model puts JSON.generate({stop_reason:, model:, served_by_fallback:}) ``` Anthropic sets safeguards for each model individually and for each policy category, in line with the model's capability: depending on the category, a flagged request may fall back to a less capable model or be declined. The `"default"` mode encodes these per-model, per-category recommendations for you, so a refused request is retried on the model Anthropic recommends for that category. Fallbacks are visible either way: the response names the model that served it, and the `fallback` content block marks the handoff. The routing is applied server-side and is not published per model on the [Models API](https://platform.claude.com/docs/en/api/models/list). To see which model served a refused request, check the response's top-level `model` field and look for a `fallback_message` entry in `usage.iterations`, as this page's samples do. Only a safety classifier decline triggers the fallback. A rate limit, overload, or server error on the requested model is returned to you as-is. The beta header must carry exactly the date `2026-07-01`, which supports both `"default"` and the explicit-list form below, or `2026-06-01`, which accepts only the explicit-list form. Under any other `server-side-fallback-*` value, the `fallbacks` parameter is rejected with a 400 error. If you built against an earlier preview of this feature, update the beta header and the request and response shapes together to the ones on this page. ### Naming your own fallback models Instead of default routing, you can set `fallbacks` to a list of up to three models. When the requested model declines, the API runs the next model in the chain on the same request. Use this form when you want to control exactly which models serve refused requests, such as pinning a model your application has qualified. ```bash cURL curl --fail-with-body -sS https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: server-side-fallback-2026-07-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-fable-5", "max_tokens": 1024, "fallbacks": [{"model": "claude-opus-4-8"}], "messages": [{"role": "user", "content": "Hello, Claude"}] }' | jq -r '.model' ``` ```bash CLI ant beta:messages create \ --model claude-fable-5 \ --max-tokens 1024 \ --message '{"role":"user","content":"Hello, Claude"}' \ --fallbacks '[{"model":"claude-opus-4-8"}]' \ --beta server-side-fallback-2026-07-01 \ --transform model --raw-output ``` ```python Python client = Anthropic() response = client.beta.messages.create( model="claude-fable-5", max_tokens=1024, messages=[{"role": "user", "content": "Hello, Claude"}], fallbacks=[{"model": "claude-opus-4-8"}], betas=["server-side-fallback-2026-07-01"], ) print(response.model) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.beta.messages.create({ model: "claude-fable-5", max_tokens: 1024, messages: [{ role: "user", content: "Hello, Claude" }], fallbacks: [{ model: "claude-opus-4-8" }], betas: ["server-side-fallback-2026-07-01"] }); console.log(response.model); ``` ```csharp C# AnthropicClient client = new(); BetaMessage response = await client.Beta.Messages.Create( new() { Model = Messages::Model.ClaudeFable5, MaxTokens = 1024, Messages = [new() { Content = "Hello, Claude", Role = Role.User }], Fallbacks = new([new(Messages::Model.ClaudeOpus4_8)]), Betas = [AnthropicBeta.ServerSideFallback2026_07_01], } ); Console.WriteLine(response.Model.Raw()); ``` ```go Go client := anthropic.NewClient() response, err := client.Beta.Messages.New(context.Background(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeFable5, MaxTokens: 1024, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Hello, Claude")), }, Fallbacks: anthropic.BetaFallbacksParamUnion{ OfBetaFallbackArray: []anthropic.BetaFallbackParam{{Model: anthropic.ModelClaudeOpus4_8}}, }, Betas: []anthropic.AnthropicBeta{anthropic.AnthropicBetaServerSideFallback2026_07_01}, }) if err != nil { panic(err) } fmt.Println(response.Model) ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); BetaMessage response = client.beta().messages().create(MessageCreateParams.builder() .model(Model.CLAUDE_FABLE_5) .maxTokens(1024L) .addUserMessage("Hello, Claude") .fallbacksOfFallbackParams(List.of(BetaFallbackParam.builder() .model(Model.CLAUDE_OPUS_4_8) .build())) .addBeta(AnthropicBeta.SERVER_SIDE_FALLBACK_2026_07_01) .build()); IO.println(response.model().asString()); ``` ```php PHP $client = new Client(); $response = $client->beta->messages->create( model: 'claude-fable-5', maxTokens: 1024, messages: [['role' => 'user', 'content' => 'Hello, Claude']], fallbacks: [['model' => 'claude-opus-4-8']], betas: ['server-side-fallback-2026-07-01'], ); echo $response->model, PHP_EOL; ``` ```ruby Ruby client = Anthropic::Client.new response = client.beta.messages.create( model: "claude-fable-5", max_tokens: 1024, messages: [{role: "user", content: "Hello, Claude"}], fallbacks: [{model: "claude-opus-4-8"}], betas: ["server-side-fallback-2026-07-01"] ) puts response.model ``` A few rules apply to the `fallbacks` list: * Entries are tried in order. Each must be distinct from the other entries and from the requested model. * Each entry must be one of the requested model's permitted targets. With the beta header set, that list is published as `allowed_fallback_models` on the model's entry in the [Models API](https://platform.claude.com/docs/en/api/models/list). * Each entry names a `model` and can override `max_tokens`, `thinking`, `output_config`, and `speed` for that attempt only. * The request must be valid as a direct request to every model named. If a fallback model does not support a feature the request uses, the API rejects the request up front. * As with the default mode, only a safety classifier decline triggers the fallback. A rate limit, overload, or server error on the requested model is returned to you as-is. The explicit-list form also works under the `server-side-fallback-2026-06-01` beta header; the `"default"` mode does not. The response has the same shape in both modes: the model that served the turn appears in the top-level `model` field, a `fallback` content block marks the handoff, and `usage.iterations` records each attempt. ### What the response contains The response looks like any other message, with two additions: * The top-level `model` field reports the model that produced the returned message, whether that is the requested model or a fallback. * A `fallback` content block marks each point in `content` where one model's output gives way to the next: `{"type": "fallback", "from": {"model": ...}, "to": {"model": ...}}`. * `from.model` echoes the model string you sent when the declining hop is the requested model. * `to.model` is always the resolved ID of the model that continues. On a refusal before any output, the `fallback` block is the first content block. For example, when default routing selects Claude Opus 4.8 for the refusal's category: ```json { "id": "msg_01XFUDYJgAACzvnptvVoYEL", "type": "message", "role": "assistant", "model": "claude-opus-4-8", "content": [ { "type": "fallback", "from": { "model": "claude-fable-5" }, "to": { "model": "claude-opus-4-8" } }, { "type": "text", "text": "Hi! How can I help you today?" } ], "stop_reason": "end_turn", "stop_details": null, "usage": { "input_tokens": 412, "output_tokens": 264, "cache_read_input_tokens": 0, "cache_creation_input_tokens": 0, "iterations": [ { "type": "message", "model": "claude-fable-5", "input_tokens": 535, "output_tokens": 0, "cache_read_input_tokens": 0, "cache_creation_input_tokens": 0 }, { "type": "fallback_message", "model": "claude-opus-4-8", "input_tokens": 412, "output_tokens": 264, "cache_read_input_tokens": 0, "cache_creation_input_tokens": 0 } ] } } ``` The `usage.iterations` array records every attempt. A model that declined appears as an ordinary `message` entry, and the model that served the turn appears as a `fallback_message` entry. If every model in the chain declines, the response is the last model's refusal, with a `message` entry for each earlier hop and a `fallback_message` entry for the last. ### Continuing the conversation On the next turn, send the assistant content back as you received it. After a mid-output fallback, `content` can include block types the declining model produced before the handoff; the following table covers which to keep and which to drop when you echo the turn. | Block type | On the next turn | | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `fallback` | Keep it exactly where it appeared. The API uses its position to validate the thinking blocks around it, so a request that echoes thinking blocks from both sides of the boundary is rejected if the block is omitted or moved. | | `text` | Keep. | | Any block after the final `fallback` block | Keep. | | `thinking`, `redacted_thinking`, or `connector_text` before the final `fallback` block | Drop. | | Client-side `tool_use` before the final `fallback` block | Drop. | | `server_tool_use` before the final `fallback` block | Keep when paired with its result. Drop when it has no matching result. | A `connector_text` block carries narration text that some tool-using responses include between tool calls. ### Streaming On a streaming request, the retry happens on the same stream, and nothing you have already received is invalidated. What you see depends on when the decline happens. **When the decline happens before any output:** * `message_start` names the fallback model, and the `fallback` block is the first content block. * Because `message_start` waits for the fallback attempt to start, time to first byte includes the declined attempt. **When the decline happens mid-output:** * The open content block closes, and the `fallback` block (an ordinary `content_block_start` and `content_block_stop` pair with no deltas) marks the boundary. * The fallback model continues from the partial output. Only the partial output's `text` blocks are passed to the fallback model as context; other block types remain in `content`. * `message_start` already named the requested model, so read the serving model from the `fallback` block's `to.model` and from the `fallback_message` entry in the final `message_delta`'s `usage.iterations`. ### Non-streaming responses On a non-streaming request, a mid-output decline behaves differently: the response omits the declined model's partial output, and the fallback model answers from scratch. The result looks like a decline before any output, with the `fallback` block first. The declined attempt and its output tokens still appear in `usage.iterations`. **Declines during tool use:** completed tool work does not block fallback. When a decline fires after server tools (for example, web search or code execution) have finished executing within a request, the fallback attempt proceeds: the completed tool results carry over, and the fallback model can keep invoking server tools. The one case that does not retry is a streaming decline that fires while a tool-use block of any type (a client tool, a server tool, or an MCP tool call) is still open on the stream: that refusal is returned directly, and if the `fallback-credit-2026-07-01` header is set it still carries a credit token redeemable by continuing the partial response. Non-streaming requests are unaffected; the API clears the partial work and retries before responding. After a conversation falls back, the API records which model served it. Later requests for that conversation that include `fallbacks` go directly to that fallback model, without running the requested model. This avoids paying for an attempt that would predictably be declined again on every turn. A few properties of the routing decision: * It is retained for approximately 1 hour and is scoped to your organization. * It is stored as a content hash of the conversation prefix plus the model that served it. The message content itself is not stored. * It is best-effort, so your code must handle the requested model being tried again at any time. A sticky-served turn carries no `fallback` content block, because no model declined that turn. Identify it by the `fallback_message` entry in `usage.iterations`, the absence of a `message` entry for the requested model, and the response's `model` field. Sticky routing applies to both streaming and non-streaming requests. On a streaming request, the routing decision is made before the stream opens, so the `message_start` event's `model` field already carries the fallback model's ID. You pay for the model that actually serves the request. An attempt that declined before producing output is not billed: its tokens are reported on its `usage.iterations` entry but not charged. Declined attempts still count against rate limits (see below). Each attempt is billed separately, at the rates of the model that ran it. The `usage.iterations` array is the per-attempt record of what you are billed. The top-level `usage` counts describe only the attempt that produced the returned message; tokens from different models are never summed into one field. Each attempt that runs counts against its own model's rate limits. If the fallback model is rate limited or overloaded, the fallback attempt is not made and the preceding refusal is returned instead. Size the fallback model's rate limits for the refusal volume you expect, or fallbacks degrade to refusals under load. When a fallback attempt is skipped this way, `stop_details.recommended_model` names a model to retry directly. The recommendation is a hint, not a guarantee, and it is `null` when no recommendation is available. ## Client-side fallback with the SDK middleware Every Anthropic SDK includes a refusal-fallback middleware. You configure it once on the client with your list of fallback models. Calls through `client.beta.messages` then retry refused requests automatically, on any platform. The middleware also sends the `fallback-credit-2026-07-01` beta header on every request it handles, so retries are repriced without per-request setup. ### Setting it up Pass the middleware to the client constructor, and share one `BetaFallbackState` instance across the requests of a conversation. ```bash cURL # The refusal-fallback middleware is an SDK feature. See the # server-side fallback section for the equivalent single-request approach, # or the fallback credit page for the raw HTTP retry pattern. ``` ```bash CLI # The refusal-fallback middleware is an SDK feature. See the # server-side fallback section for the equivalent single-request approach, # or the fallback credit page for the raw HTTP retry pattern. ``` ```python Python from anthropic import Anthropic, BetaFallbackState, BetaRefusalFallbackMiddleware # On a refusal, the middleware retries on the listed fallback model and # automatically sends the fallback-credit beta header on every request it handles. client = Anthropic( middleware=[BetaRefusalFallbackMiddleware([{"model": "claude-opus-4-8"}])], ) state = BetaFallbackState() # pins follow-ups to the model that accepted # Streaming: on a refusal the middleware retries on the fallback model and # splices its events onto the open stream. with ( state, client.beta.messages.stream( max_tokens=1024, model="claude-fable-5", messages=[{"role": "user", "content": "Hello, Claude"}], ) as stream, ): for text in stream.text_stream: print(text, end="", flush=True) final_message = stream.get_final_message() print(f"\nserved by: {final_message.model}") # Non-streaming: reusing the state keeps the conversation pinned. with state: message = client.beta.messages.create( max_tokens=1024, model="claude-fable-5", messages=[{"role": "user", "content": "Hello, Claude"}], ) print(f"served by: {message.model}") ``` ```typescript TypeScript import { BetaFallbackState, betaRefusalFallbackMiddleware } from "@anthropic-ai/sdk"; // On a refusal, the middleware retries on the listed fallback model and // automatically sends the fallback-credit beta header on every request it handles. const client = new Anthropic({ middleware: [betaRefusalFallbackMiddleware([{ model: "claude-opus-4-8" }])] }); // Share one state across the conversation so follow-up requests stay // pinned to the model that accepted. const fallbackState = new BetaFallbackState(); // Streaming: on a refusal the middleware retries on the fallback model and // splices its events onto the open stream. const stream = client.beta.messages .stream( { max_tokens: 1024, model: "claude-fable-5", messages: [{ role: "user", content: "Hello, Claude" }] }, { fallbackState } ) .on("text", (text) => process.stdout.write(text)); const finalMessage = await stream.finalMessage(); console.log("\nserved by:", finalMessage.model); // Non-streaming: reusing the state keeps the conversation pinned. const message = await client.beta.messages.create( { max_tokens: 1024, model: "claude-fable-5", messages: [{ role: "user", content: "Hello, Claude" }] }, { fallbackState } ); console.log("served by:", message.model); ``` ```csharp C# using Anthropic; using Anthropic.Helpers; using Anthropic.Models.Beta.Messages; using Messages = Anthropic.Models.Messages; // On a refusal, the handler retries on the listed fallback model and // automatically sends the fallback-credit beta header on every request it handles. AnthropicClient client = new() { Handlers = [ new BetaRefusalFallbackHandler { Fallbacks = [new(Messages::Model.ClaudeOpus4_8)] }, ], }; // Pins follow-up requests sharing this state to the model that accepted. BetaFallbackState fallbackState = BetaFallbackState.Create(); MessageCreateParams parameters = new() { Model = Messages::Model.ClaudeFable5, MaxTokens = 1024, Messages = [new() { Content = "Hello, Claude", Role = Role.User }], }; // Streaming: if the stream ends in a refusal, the handler splices the fallback // model's events onto the still-open stream. BetaMessageContentAggregator aggregator = new(); using (fallbackState.Use()) { var responseUpdates = client.Beta.Messages.CreateStreaming(parameters); await foreach (BetaRawMessageStreamEvent rawEvent in responseUpdates.CollectAsync(aggregator)) { if ( rawEvent.TryPickContentBlockDelta(out var deltaEvent) && deltaEvent.Delta.TryPickText(out var textDelta) ) { Console.Write(textDelta.Text); } } } BetaMessage streamedMessage = aggregator.Message(); Console.WriteLine($"\nserved by: {streamedMessage.Model.Raw()}"); // Non-streaming: reusing the state keeps the conversation pinned to the model that accepted. using (fallbackState.Use()) { BetaMessage message = await client.Beta.Messages.Create(parameters); Console.WriteLine($"served by: {message.Model.Raw()}"); } ``` ```go Go import ( // ... "github.com/anthropics/anthropic-sdk-go/lib/betafallback" // ... ) func main() { ctx := context.Background() // The middleware retries a refused request on each fallback model in // turn, and opts requests into the fallback-credit beta automatically. client := anthropic.NewClient( option.WithMiddleware(betafallback.BetaRefusalFallbackMiddleware( []anthropic.BetaFallbackParam{{Model: anthropic.ModelClaudeOpus4_8}}, )), ) // One state per conversation: requests sharing it stay pinned to the // model that accepted, so a follow-up never re-asks a model that refused. state := &betafallback.BetaFallbackState{} conversation := betafallback.WithBetaFallbackState(state) params := anthropic.BetaMessageNewParams{ MaxTokens: 1024, Model: anthropic.ModelClaudeFable5, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Hello, Claude")), }, } // Streaming: on a refusal the middleware retries in place, splicing the // fallback model's events onto the open stream as one continuous message. stream := client.Beta.Messages.NewStreaming(ctx, params, conversation) defer stream.Close() var streamed anthropic.BetaMessage for stream.Next() { event := stream.Current() if err := streamed.Accumulate(event); err != nil { panic(err) } switch eventVariant := event.AsAny().(type) { case anthropic.BetaRawContentBlockDeltaEvent: if textDelta, ok := eventVariant.Delta.AsAny().(anthropic.BetaTextDelta); ok { fmt.Print(textDelta.Text) } } } if err := stream.Err(); err != nil { panic(err) } fmt.Println("\nserved by:", streamed.Model) // Non-streaming: the shared state pins this follow-up to the model that // served the streamed turn. message, err := client.Beta.Messages.New(ctx, params, conversation) if err != nil { panic(err) } fmt.Println("served by:", message.Model) } ``` ```java Java import com.anthropic.client.AnthropicClient; import com.anthropic.client.okhttp.AnthropicOkHttpClient; import com.anthropic.core.RequestOptions; import com.anthropic.core.http.StreamResponse; import com.anthropic.helpers.BetaFallbackState; import com.anthropic.helpers.BetaMessageAccumulator; import com.anthropic.helpers.BetaRefusalFallbackInterceptor; import com.anthropic.models.beta.messages.BetaMessage; import com.anthropic.models.beta.messages.BetaRawMessageStreamEvent; import com.anthropic.models.beta.messages.MessageCreateParams; import com.anthropic.models.messages.Model; void main() { // The interceptor retries refused requests on the fallback model. It automatically // adds the fallback-credit beta header to every request it handles. AnthropicClient client = AnthropicOkHttpClient.builder() .fromEnv() .addInterceptor(BetaRefusalFallbackInterceptor.builder() .addFallback(Model.CLAUDE_OPUS_4_8) .build()) .build(); // Share one state across requests so follow-ups stay pinned to the model that accepted. BetaFallbackState state = BetaFallbackState.create(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_FABLE_5) .maxTokens(1024) .addUserMessage("Hello, Claude") .build(); // Streaming: on a refusal, the fallback model's events are spliced onto the open stream. BetaMessageAccumulator accumulator = BetaMessageAccumulator.create(); try (StreamResponse streamResponse = client.beta() .messages() .createStreaming(params, RequestOptions.builder().fallbackState(state).build())) { streamResponse.stream() .peek(accumulator::accumulate) .forEach(event -> event.contentBlockDelta() .flatMap(deltaEvent -> deltaEvent.delta().text()) .ifPresent(textDelta -> IO.print(textDelta.text()))); } IO.println("\nserved by: " + accumulator.message().model().asString()); // Non-streaming: reusing the same state keeps the conversation pinned. BetaMessage message = client.beta() .messages() .create(params, RequestOptions.builder().fallbackState(state).build()); IO.println("served by: " + message.model().asString()); } ``` ```php PHP use Anthropic\Beta\Messages\BetaRawContentBlockDeltaEvent; use Anthropic\Beta\Messages\BetaTextDelta; use Anthropic\Client; use Anthropic\Lib\Middleware\BetaFallbackState; use Anthropic\Lib\Middleware\RefusalFallbackMiddleware; use Anthropic\Lib\Streaming\MessageAccumulator; // Configure the fallback chain once. On a refusal, the middleware retries the // request down the chain and sends the fallback-credit beta header for you. $client = new Client( requestOptions: [ 'middleware' => [new RefusalFallbackMiddleware([['model' => 'claude-opus-4-8']])], ], ); // Share one state across the conversation so follow-up requests stay pinned // to the model that accepted. $state = new BetaFallbackState(); // Streaming: on a refusal the middleware splices the fallback model's events // onto the still-open stream. The accumulator's model is the serving model. $stream = $client->beta->messages->createStream( model: 'claude-fable-5', maxTokens: 1024, messages: [['role' => 'user', 'content' => 'Hello, Claude']], requestOptions: ['fallbackState' => $state], ); $accumulator = MessageAccumulator::forBetaMessages(); foreach ($stream as $event) { $accumulator->accumulate($event); if ($event instanceof BetaRawContentBlockDeltaEvent && $event->delta instanceof BetaTextDelta) { echo $event->delta->text; } } echo "\nserved by: {$accumulator->message()->model}\n"; // Non-streaming: same middleware. Reusing the state keeps the conversation // pinned to the model that accepted. $message = $client->beta->messages->create( model: 'claude-fable-5', maxTokens: 1024, messages: [['role' => 'user', 'content' => 'Hello, Claude']], requestOptions: ['fallbackState' => $state], ); echo "served by: {$message->model}\n"; ``` ```ruby Ruby # On a refusal, the middleware retries the request down the fallback chain. # It sends the fallback-credit beta header on every request it handles. client = Anthropic::Client.new( middleware: [Anthropic::BetaRefusalFallbackMiddleware.new([{model: "claude-opus-4-8"}])] ) # Share one state across the conversation so follow-up requests stay # pinned to the model that accepted. state = Anthropic::BetaFallbackState.new # Streaming: on a refusal the middleware splices the fallback model's # events onto the still-open stream. stream = client.beta.messages.stream( model: "claude-fable-5", max_tokens: 1024, messages: [{role: "user", content: "Hello, Claude"}], request_options: {fallback_state: state} ) stream.text.each { print it } puts "\nserved by: #{stream.accumulated_message.model}" # Non-streaming: reusing the state keeps the conversation pinned to the model that accepted. message = client.beta.messages.create( model: "claude-fable-5", max_tokens: 1024, messages: [{role: "user", content: "Hello, Claude"}], request_options: {fallback_state: state} ) puts "served by: #{message.model}" ``` ### How it behaves * Retries walk your fallback list in order. A fallback model that itself refuses passes the request to the next entry. * When every model in the list has declined, the middleware returns the final refusal (the last model's refusal response) rather than raising an error. * [Thinking blocks from Claude Fable 5](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-output-on-claude-fable-5-and-claude-mythos-5) pass through unchanged: each retry re-sends your original request body, and the only blocks the middleware removes from conversation history on later requests are the `fallback` boundary blocks it added itself. * Responses served through the middleware include a `fallback` content block at each model boundary, the same as server-side fallback responses. The middleware manages those blocks for you on later requests. * The model that accepted is recorded in `BetaFallbackState`, so follow-up requests that share the state stay pinned to it rather than re-asking a model that refused. The middleware and the server-side `fallbacks` parameter do the same job. Configure one or the other, never both on the same request. To send a server-side `fallbacks` request from an application that installs the middleware, use a separate client instance without it. Over raw HTTP or with custom retry logic, implement the pattern the middleware wraps: Check the response for `stop_reason: "refusal"`. Send the same request with `model` set to a fallback model, such as Claude Opus 4.8. A request that Claude Fable 5's classifiers decline can normally be served by another model. How you handle the conversation history depends on whether you redeem a [fallback credit](https://platform.claude.com/docs/en/build-with-claude/fallback-credit): * **Not redeeming a credit:** you can first strip the [thinking blocks from Claude Fable 5](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-output-on-claude-fable-5-and-claude-mythos-5) out of the conversation history. Other models ignore them, and stripping keeps cross-model requests minimal. * **Redeeming a credit:** send the body unchanged, because redemption requires an exact match. For multi-turn conversations, keep using the fallback model for subsequent turns rather than switching back. A manual retry writes the fallback model's prompt cache from scratch, which costs more than reading an existing cache. [Fallback credit](https://platform.claude.com/docs/en/build-with-claude/fallback-credit) refunds that cost; redeem it on every retry you build yourself. ## Refusals in Message Batches A refused request in a [Message Batch](https://platform.claude.com/docs/en/build-with-claude/batch-processing) comes back as `result.type: "succeeded"` with `stop_reason: "refusal"`. Batch results carry the same `stop_details` object as synchronous responses, so you can detect refusals through either `stop_reason` or `stop_details.type`. One difference: batch refusals don't mint fallback credits, so `stop_details` on a batch result never includes a `fallback_credit_token`. Server-side fallback is not available for batches (a batch request that includes `fallbacks` produces a per-item errored result). To retry refused batch items: 1. Collect the refused items from the results. 2. Strip Claude Fable 5's thinking blocks from any multi-turn histories. 3. Resubmit them on a fallback model as a new batch or as direct requests. ## Common pitfalls * **Retry on a different model.** Re-sending a refused request to the same model usually earns another refusal. Point the retry at the fallback model. * **Budget retries per request, not per turn or per session.** A single turn can produce several refusals, for example an agent plus its sub-agents. * **Configure fallback on every request path.** Retry handlers, error-recovery branches, and background workers all need it. A handler that re-issues a request without fallback loses the protection on exactly the requests most likely to need it. * **Give sub-agent calls their own fallback.** The `fallbacks` parameter does not propagate into model calls made from inside tool execution. * **Make fallback a property of the request, not of ambient state.** A shared flag, cached config value, or global toggle can drift out of sync and silently leave a request unprotected. When you cannot confirm fallback is active, configure it rather than assume it is on. * **Instrument refusals as their own signal.** A refusal is an HTTP 200, so monitoring built on error rates or 5xx responses never sees it. Emit one event per refusal and one per fallback-served response (the `fallback_message` entry in `usage.iterations` marks the latter), then alert on the gap between the two counts. * **Branch on `stop_reason` or `stop_details.type`, not on `content` or the inner `stop_details` fields.** The `stop_details` object is always present on a refusal, but its `category` and `explanation` fields can be `null`. Check for `stop_reason` equal to `"refusal"` directly. ## Next steps Avoid paying the prompt-cache cost twice when you build the retry yourself. Every `stop_reason` value and how to handle it. How SDK middleware works, including the refusal-fallback helper. Move an existing application to Claude Fable 5. --- title: Stop reasons and fallback url: https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons description: Learn what each stop_reason value means and how to handle truncation, tool use, paused turns, and refusals in your application. --- Every Messages API response includes a `stop_reason` field that tells you why Claude stopped generating. Check this field to decide whether to use the response as-is, continue the conversation, retry, or fall back to another model. For the full response schema, see the [Messages API reference](https://platform.claude.com/docs/en/api/messages/create). ## Quick reference | Value | When it occurs | What to do | | -------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`end_turn`](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons#end-turn) | Claude finished its response naturally. | Use the response. | | [`max_tokens`](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons#max-tokens) | The response reached your `max_tokens` limit. | Raise `max_tokens` or [continue the response](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons#ensuring-complete-responses). | | [`stop_sequence`](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons#stop-sequence) | Claude emitted one of your `stop_sequences`. | Read `stop_sequence` to see which one fired. | | [`tool_use`](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons#tool-use) | Claude is calling a tool. | Run the tool and return the result. A server tool call still missing its result block completes in a later response. | | [`pause_turn`](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons#pause-turn) | A server-tool loop reached its iteration limit. | Send the assistant content back to continue. | | [`refusal`](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons#refusal) | Claude declined to respond. | Read `stop_details` and [retry on a fallback model](https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback). | | [`model_context_window_exceeded`](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons#model-context-window-exceeded) | The response filled the model's context window. | Treat the response as truncated. | ## The stop\_reason field The `stop_reason` field is part of every successful Messages API response. Unlike errors, which indicate failures in processing your request, `stop_reason` tells you why Claude completed its response generation. ```json Example response { "id": "msg_01234", "type": "message", "role": "assistant", "content": [ { "type": "text", "text": "Here's the answer to your question..." } ], "stop_reason": "end_turn", "stop_sequence": null, "stop_details": null, "usage": { "input_tokens": 100, "output_tokens": 50 } } ``` ## Stop reason values ### end\_turn The most common stop reason. Indicates Claude finished its response naturally. ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "messages": [{"role": "user", "content": "Hello!"}] }' | jq 'if .stop_reason == "end_turn" then (.content[] | select(.type == "text") | .text) else . end' ``` ```bash CLI ant messages create \ --model claude-opus-5 \ --max-tokens 1024 \ --message '{role: user, content: "Hello!"}' \ --format json | jq 'if .stop_reason == "end_turn" then (.content[] | select(.type == "text") | .text) else . end' ``` ```python Python client = anthropic.Anthropic() response = client.messages.create( model="claude-opus-5", max_tokens=1024, messages=[{"role": "user", "content": "Hello!"}], ) if response.stop_reason == "end_turn": # Process the complete response for block in response.content: if block.type == "text": print(block.text) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, messages: [{ role: "user", content: "Hello!" }] }); if (response.stop_reason === "end_turn") { // Process the complete response const textBlock = response.content.find( (block): block is Anthropic.TextBlock => block.type === "text" ); console.log(textBlock?.text); } ``` ```csharp C# AnthropicClient client = new(); var response = await client.Messages.Create(new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [new() { Role = Role.User, Content = "Hello!" }] }); if (response.StopReason == "end_turn") { // Process the complete response foreach (var block in response.Content) { if (block.TryPickText(out var textBlock)) { Console.WriteLine(textBlock.Text); } } } ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Hello!")), }, }) if err != nil { log.Fatal(err) } if response.StopReason == "end_turn" { // Process the complete response for _, block := range response.Content { if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok { fmt.Println(textBlock.Text) } } } ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); Message response = client.messages().create( MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addUserMessage("Hello!") .build() ); if (response.stopReason().map(StopReason.END_TURN::equals).orElse(false)) { // Process the complete response response.content().stream() .flatMap(block -> block.text().stream()) .forEach(textBlock -> IO.println(textBlock.text())); } ``` ```php PHP $client = new Client(); $response = $client->messages->create( maxTokens: 1024, messages: [['role' => 'user', 'content' => 'Hello!']], model: 'claude-opus-5', ); if ($response->stopReason === 'end_turn') { // Process the complete response foreach ($response->content as $block) { if ($block->type === 'text') { echo $block->text, PHP_EOL; } } } ``` ```ruby Ruby client = Anthropic::Client.new response = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [{ role: "user", content: "Hello!" }] ) if response.stop_reason == :end_turn # Process the complete response response.content.each do |block| puts block.text if block.type == :text end end ``` Sometimes Claude returns an empty response (exactly 2–3 tokens with no content) with `stop_reason: "end_turn"`. This typically occurs when Claude interprets that the assistant turn is complete, particularly after tool results. **Common causes:** * Adding text blocks immediately after tool results (Claude learns to expect the user to always insert text after tool results, so it ends its turn to follow the pattern) * Sending Claude's completed response back without adding anything (Claude already determined it's done, so it will remain done) **How to prevent empty responses:** ```python Python # INCORRECT: Adding text immediately after tool_result messages = [ {"role": "user", "content": "Calculate the sum of 1234 and 5678"}, { "role": "assistant", "content": [ { "type": "tool_use", "id": "toolu_123", "name": "calculator", "input": {"operation": "add", "a": 1234, "b": 5678}, } ], }, { "role": "user", "content": [ {"type": "tool_result", "tool_use_id": "toolu_123", "content": "6912"}, { "type": "text", "text": "Here's the result", # Don't add text after tool_result }, ], }, ] # CORRECT: Send tool results directly without additional text messages = [ {"role": "user", "content": "Calculate the sum of 1234 and 5678"}, { "role": "assistant", "content": [ { "type": "tool_use", "id": "toolu_123", "name": "calculator", "input": {"operation": "add", "a": 1234, "b": 5678}, } ], }, { "role": "user", "content": [ {"type": "tool_result", "tool_use_id": "toolu_123", "content": "6912"} ], }, # Just the tool_result, no additional text ] ``` ```typescript TypeScript // INCORRECT: Adding text immediately after tool_result let messages: Anthropic.MessageParam[] = [ { role: "user", content: "Calculate the sum of 1234 and 5678" }, { role: "assistant", content: [ { type: "tool_use", id: "toolu_123", name: "calculator", input: { operation: "add", a: 1234, b: 5678 } } ] }, { role: "user", content: [ { type: "tool_result", tool_use_id: "toolu_123", content: "6912" }, { type: "text", text: "Here's the result" } // Don't add text after tool_result ] } ]; // CORRECT: Send tool results directly without additional text messages = [ { role: "user", content: "Calculate the sum of 1234 and 5678" }, { role: "assistant", content: [ { type: "tool_use", id: "toolu_123", name: "calculator", input: { operation: "add", a: 1234, b: 5678 } } ] }, { role: "user", // Just the tool_result, no additional text content: [{ type: "tool_result", tool_use_id: "toolu_123", content: "6912" }] } ]; ``` ```csharp C# using System.Text.Json; using Anthropic.Models.Messages; var input = JsonSerializer.Deserialize>( """{"operation":"add","a":1234,"b":5678}""" )!; // INCORRECT: Adding text immediately after tool_result List messages = [ new() { Role = Role.User, Content = "Calculate the sum of 1234 and 5678" }, new() { Role = Role.Assistant, Content = new List { new ToolUseBlockParam { ID = "toolu_123", Name = "calculator", Input = input } } }, new() { Role = Role.User, Content = new List { new ToolResultBlockParam { ToolUseID = "toolu_123", Content = "6912" }, new TextBlockParam { Text = "Here's the result" } // Don't add text after tool_result } } ]; // CORRECT: Send tool results directly without additional text messages = [ new() { Role = Role.User, Content = "Calculate the sum of 1234 and 5678" }, new() { Role = Role.Assistant, Content = new List { new ToolUseBlockParam { ID = "toolu_123", Name = "calculator", Input = input } } }, new() { Role = Role.User, // Just the tool_result, no additional text Content = new List { new ToolResultBlockParam { ToolUseID = "toolu_123", Content = "6912" } } } ]; ``` ```go Go input := map[string]any{"operation": "add", "a": 1234, "b": 5678} // INCORRECT: Adding text immediately after tool_result messages := []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Calculate the sum of 1234 and 5678")), anthropic.NewAssistantMessage( anthropic.NewToolUseBlock("toolu_123", input, "calculator"), ), anthropic.NewUserMessage( anthropic.NewToolResultBlock("toolu_123", "6912", false), anthropic.NewTextBlock("Here's the result"), // Don't add text after tool_result ), } // CORRECT: Send tool results directly without additional text messages = []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Calculate the sum of 1234 and 5678")), anthropic.NewAssistantMessage( anthropic.NewToolUseBlock("toolu_123", input, "calculator"), ), // Just the tool_result, no additional text anthropic.NewUserMessage( anthropic.NewToolResultBlock("toolu_123", "6912", false), ), } ``` ```java Java ToolUseBlockParam toolUse = ToolUseBlockParam.builder() .id("toolu_123") .name("calculator") .input(ToolUseBlockParam.Input.builder() .putAdditionalProperty("operation", JsonValue.from("add")) .putAdditionalProperty("a", JsonValue.from(1234)) .putAdditionalProperty("b", JsonValue.from(5678)) .build()) .build(); // INCORRECT: Adding text immediately after tool_result List messages = List.of( MessageParam.builder().role(MessageParam.Role.USER) .content("Calculate the sum of 1234 and 5678").build(), MessageParam.builder().role(MessageParam.Role.ASSISTANT) .contentOfBlockParams(List.of(ContentBlockParam.ofToolUse(toolUse))).build(), MessageParam.builder().role(MessageParam.Role.USER) .contentOfBlockParams(List.of( ContentBlockParam.ofToolResult( ToolResultBlockParam.builder().toolUseId("toolu_123").content("6912").build()), // Don't add text after tool_result ContentBlockParam.ofText(TextBlockParam.builder().text("Here's the result").build()) )).build() ); // CORRECT: Send tool results directly without additional text messages = List.of( MessageParam.builder().role(MessageParam.Role.USER) .content("Calculate the sum of 1234 and 5678").build(), MessageParam.builder().role(MessageParam.Role.ASSISTANT) .contentOfBlockParams(List.of(ContentBlockParam.ofToolUse(toolUse))).build(), // Just the tool_result, no additional text MessageParam.builder().role(MessageParam.Role.USER) .contentOfBlockParams(List.of( ContentBlockParam.ofToolResult( ToolResultBlockParam.builder().toolUseId("toolu_123").content("6912").build()) )).build() ); ``` ```php PHP // INCORRECT: Adding text immediately after tool_result $messages = [ ['role' => 'user', 'content' => 'Calculate the sum of 1234 and 5678'], [ 'role' => 'assistant', 'content' => [ [ 'type' => 'tool_use', 'id' => 'toolu_123', 'name' => 'calculator', 'input' => ['operation' => 'add', 'a' => 1234, 'b' => 5678], ], ], ], [ 'role' => 'user', 'content' => [ ['type' => 'tool_result', 'tool_use_id' => 'toolu_123', 'content' => '6912'], // Don't add text after tool_result ['type' => 'text', 'text' => "Here's the result"], ], ], ]; // CORRECT: Send tool results directly without additional text $messages = [ ['role' => 'user', 'content' => 'Calculate the sum of 1234 and 5678'], [ 'role' => 'assistant', 'content' => [ [ 'type' => 'tool_use', 'id' => 'toolu_123', 'name' => 'calculator', 'input' => ['operation' => 'add', 'a' => 1234, 'b' => 5678], ], ], ], [ 'role' => 'user', // Just the tool_result, no additional text 'content' => [ ['type' => 'tool_result', 'tool_use_id' => 'toolu_123', 'content' => '6912'], ], ], ]; ``` ```ruby Ruby # INCORRECT: Adding text immediately after tool_result messages = [ { role: "user", content: "Calculate the sum of 1234 and 5678" }, { role: "assistant", content: [ { type: "tool_use", id: "toolu_123", name: "calculator", input: { operation: "add", a: 1234, b: 5678 } } ] }, { role: "user", content: [ { type: "tool_result", tool_use_id: "toolu_123", content: "6912" }, # Don't add text after tool_result { type: "text", text: "Here's the result" } ] } ] # CORRECT: Send tool results directly without additional text messages = [ { role: "user", content: "Calculate the sum of 1234 and 5678" }, { role: "assistant", content: [ { type: "tool_use", id: "toolu_123", name: "calculator", input: { operation: "add", a: 1234, b: 5678 } } ] }, { role: "user", # Just the tool_result, no additional text content: [ { type: "tool_result", tool_use_id: "toolu_123", content: "6912" } ] } ] ``` If you still get empty responses after fixing the message structure, add a continuation prompt in a new user message rather than retrying with the empty response: ```python Python def handle_empty_response(client, messages): response = client.messages.create( model="claude-opus-5", max_tokens=1024, messages=messages ) # Check if response is empty if response.stop_reason == "end_turn" and not response.content: # INCORRECT: Don't just retry with the empty response # This won't work because Claude already decided it's done # CORRECT: Add a continuation prompt in a NEW user message messages.append({"role": "user", "content": "Please continue"}) response = client.messages.create( model="claude-opus-5", max_tokens=1024, messages=messages ) return response ``` ```typescript TypeScript async function handleEmptyResponse( client: Anthropic, messages: Anthropic.MessageParam[] ): Promise { let response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, messages }); // Check if response is empty if (response.stop_reason === "end_turn" && response.content.length === 0) { // INCORRECT: Don't just retry with the empty response // This won't work because Claude already decided it's done // CORRECT: Add a continuation prompt in a NEW user message messages.push({ role: "user", content: "Please continue" }); response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, messages }); } return response; } ``` ```csharp C# static async Task HandleEmptyResponse(AnthropicClient client, List messages) { var response = await client.Messages.Create(new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = messages }); // Check if response is empty if (response.StopReason == "end_turn" && response.Content.Count == 0) { // CORRECT: Add a continuation prompt in a NEW user message messages.Add(new() { Role = Role.User, Content = "Please continue" }); response = await client.Messages.Create(new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = messages }); } return response; } ``` ```go Go func handleEmptyResponse(client anthropic.Client, messages []anthropic.MessageParam) (*anthropic.Message, error) { response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: messages, }) if err != nil { return nil, err } // Check if response is empty if response.StopReason == "end_turn" && len(response.Content) == 0 { // CORRECT: Add a continuation prompt in a NEW user message messages = append(messages, anthropic.NewUserMessage(anthropic.NewTextBlock("Please continue"))) response, err = client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: messages, }) if err != nil { return nil, err } } return response, nil } ``` ```java Java static Message handleEmptyResponse(AnthropicClient client, List messages) { Message response = client.messages().create( MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .messages(messages) .build() ); // Check if response is empty boolean isEndTurn = response.stopReason().map(StopReason.END_TURN::equals).orElse(false); if (isEndTurn && response.content().isEmpty()) { // CORRECT: Add a continuation prompt in a NEW user message List extended = new ArrayList<>(messages); extended.add(MessageParam.builder() .role(MessageParam.Role.USER) .content("Please continue") .build()); response = client.messages().create( MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .messages(extended) .build() ); } return response; } ``` ```php PHP function handle_empty_response(Client $client, array $messages) { $response = $client->messages->create( maxTokens: 1024, messages: $messages, model: 'claude-opus-5', ); // Check if response is empty if ($response->stopReason === 'end_turn' && count($response->content) === 0) { // CORRECT: Add a continuation prompt in a NEW user message $messages[] = ['role' => 'user', 'content' => 'Please continue']; $response = $client->messages->create( maxTokens: 1024, messages: $messages, model: 'claude-opus-5', ); } return $response; } ``` ```ruby Ruby def handle_empty_response(client, messages) response = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: messages ) # Check if response is empty if response.stop_reason == :end_turn && response.content.empty? # CORRECT: Add a continuation prompt in a NEW user message messages << { role: "user", content: "Please continue" } response = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: messages ) end response end ``` **Best practices:** 1. **Never add text blocks immediately after tool results:** This teaches Claude to expect user input after every tool use. 2. **Don't retry empty responses without modification:** Sending the empty response back won't help. 3. **Use continuation prompts as a last resort:** Only if these fixes don't resolve the issue. ### max\_tokens Claude stopped because it reached the `max_tokens` limit specified in your request. ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 10, "messages": [{"role": "user", "content": "Explain quantum physics"}] }' | jq '.stop_reason' ``` ```bash CLI ant messages create \ --model claude-opus-5 \ --max-tokens 10 \ --message '{role: user, content: "Explain quantum physics"}' \ --format json | jq '.stop_reason' ``` ```python Python client = anthropic.Anthropic() # Request with limited tokens response = client.messages.create( model="claude-opus-5", max_tokens=10, messages=[{"role": "user", "content": "Explain quantum physics"}], ) if response.stop_reason == "max_tokens": # Response was truncated print("Response was cut off at token limit") # Consider making another request to continue ``` ```typescript TypeScript const client = new Anthropic(); // Request with limited tokens const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 10, messages: [{ role: "user", content: "Explain quantum physics" }] }); if (response.stop_reason === "max_tokens") { // Response was truncated console.log("Response was cut off at token limit"); // Consider making another request to continue } ``` ```csharp C# AnthropicClient client = new(); // Request with limited tokens var response = await client.Messages.Create(new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 10, Messages = [new() { Role = Role.User, Content = "Explain quantum physics" }] }); if (response.StopReason == "max_tokens") { // Response was truncated Console.WriteLine("Response was cut off at token limit"); // Consider making another request to continue } ``` ```go Go client := anthropic.NewClient() // Request with limited tokens response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 10, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Explain quantum physics")), }, }) if err != nil { log.Fatal(err) } if response.StopReason == "max_tokens" { // Response was truncated fmt.Println("Response was cut off at token limit") // Consider making another request to continue } ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); // Request with limited tokens Message response = client.messages().create( MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(10L) .addUserMessage("Explain quantum physics") .build() ); if (response.stopReason().map(StopReason.MAX_TOKENS::equals).orElse(false)) { // Response was truncated IO.println("Response was cut off at token limit"); // Consider making another request to continue } ``` ```php PHP $client = new Client(); // Request with limited tokens $response = $client->messages->create( maxTokens: 10, messages: [['role' => 'user', 'content' => 'Explain quantum physics']], model: 'claude-opus-5', ); if ($response->stopReason === 'max_tokens') { // Response was truncated echo 'Response was cut off at token limit', PHP_EOL; // Consider making another request to continue } ``` ```ruby Ruby client = Anthropic::Client.new # Request with limited tokens response = client.messages.create( model: "claude-opus-5", max_tokens: 10, messages: [{ role: "user", content: "Explain quantum physics" }] ) if response.stop_reason == :max_tokens # Response was truncated puts "Response was cut off at token limit" # Consider making another request to continue end ``` If Claude's response is cut off because it hit the `max_tokens` limit, and the truncated response contains an incomplete tool use block, you'll need to retry the request with a higher `max_tokens` value to get the full tool use. ```bash CLI RESPONSE=$(ant messages create --max-tokens 1024 \ --format jsonl < request.yaml) # Check if the response was truncated mid tool use STOP_REASON=$(jq -r '.stop_reason' <<<"$RESPONSE") LAST_TYPE=$(jq -r '.content[-1].type' <<<"$RESPONSE") if [ "$STOP_REASON" = "max_tokens" ] && [ "$LAST_TYPE" = "tool_use" ]; then # Retry with a higher max_tokens ant messages create --max-tokens 4096 < request.yaml fi ``` ```python Python # Check if response was truncated during tool use if response.stop_reason == "max_tokens": # Check if the last content block is an incomplete tool_use last_block = response.content[-1] if last_block.type == "tool_use": # Send the request with higher max_tokens response = client.messages.create( model="claude-opus-5", max_tokens=4096, # Increased limit messages=messages, tools=tools, ) ``` ```typescript TypeScript // Check if response was truncated during tool use if (response.stop_reason === "max_tokens") { // Check if the last content block is an incomplete tool_use const lastBlock = response.content[response.content.length - 1]; if (lastBlock.type === "tool_use") { // Send the request with higher max_tokens response = await client.messages.create({ model: "claude-opus-5", max_tokens: 4096, // Increased limit messages: messages, tools: tools }); } } ``` ```csharp C# using System.Linq; using Anthropic; using Anthropic.Models.Messages; AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = messages, Tools = tools }; var response = await client.Messages.Create(parameters); if (response.StopReason == "max_tokens") { var lastBlock = response.Content.Last(); if (lastBlock.TryPickToolUse(out _)) { response = await client.Messages.Create(parameters with { MaxTokens = 4096 }); } } ``` ```go Go response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: messages, Tools: tools, }) if err != nil { log.Fatal(err) } if response.StopReason == "max_tokens" { lastBlock := response.Content[len(response.Content)-1] switch lastBlock.AsAny().(type) { case anthropic.ToolUseBlock: response, err = client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 4096, Messages: messages, Tools: tools, }) if err != nil { log.Fatal(err) } } } ``` ```java Java // Check if response was truncated during tool use if (response.stopReason().isPresent() && response.stopReason().get().equals(StopReason.MAX_TOKENS)) { ContentBlock lastBlock = response.content().get(response.content().size() - 1); if (lastBlock.toolUse().isPresent()) { // Send the request with higher max_tokens response = client.messages().create( MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(4096L) // Increased limit .messages(messages) .tools(tools) .build() ); } } ``` ```php PHP $response = $client->messages->create( maxTokens: 1024, messages: $messages, model: 'claude-opus-5', tools: $tools, ); if ($response->stopReason === 'max_tokens') { $lastBlock = end($response->content); if ($lastBlock->type === 'tool_use') { $response = $client->messages->create( maxTokens: 4096, messages: $messages, model: 'claude-opus-5', tools: $tools, ); } } ``` ```ruby Ruby response = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: messages, tools: tools ) if response.stop_reason == :max_tokens last_block = response.content.last if last_block.type == :tool_use response = client.messages.create( model: "claude-opus-5", max_tokens: 4096, messages: messages, tools: tools ) end end ``` ### stop\_sequence Claude encountered one of your custom stop sequences. ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "stop_sequences": ["END", "STOP"], "messages": [{"role": "user", "content": "Generate text until you say END"}] }' | jq '{stop_reason, stop_sequence}' ``` ```bash CLI ant messages create \ --model claude-opus-5 \ --max-tokens 1024 \ --stop-sequence END --stop-sequence STOP \ --message '{role: user, content: "Generate text until you say END"}' \ --format json | jq '{stop_reason, stop_sequence}' ``` ```python Python client = anthropic.Anthropic() response = client.messages.create( model="claude-opus-5", max_tokens=1024, stop_sequences=["END", "STOP"], messages=[{"role": "user", "content": "Generate text until you say END"}], ) if response.stop_reason == "stop_sequence": print(f"Stopped at sequence: {response.stop_sequence}") ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, stop_sequences: ["END", "STOP"], messages: [{ role: "user", content: "Generate text until you say END" }] }); if (response.stop_reason === "stop_sequence") { console.log(`Stopped at sequence: ${response.stop_sequence}`); } ``` ```csharp C# AnthropicClient client = new(); var response = await client.Messages.Create(new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, StopSequences = ["END", "STOP"], Messages = [new() { Role = Role.User, Content = "Generate text until you say END" }] }); if (response.StopReason == "stop_sequence") { Console.WriteLine($"Stopped at sequence: {response.StopSequence}"); } ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, StopSequences: []string{"END", "STOP"}, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Generate text until you say END")), }, }) if err != nil { log.Fatal(err) } if response.StopReason == "stop_sequence" { fmt.Printf("Stopped at sequence: %s\n", response.StopSequence) } ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); Message response = client.messages().create( MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addStopSequence("END") .addStopSequence("STOP") .addUserMessage("Generate text until you say END") .build() ); if (response.stopReason().map(StopReason.STOP_SEQUENCE::equals).orElse(false)) { IO.println("Stopped at sequence: " + response.stopSequence().orElse("")); } ``` ```php PHP $client = new Client(); $response = $client->messages->create( maxTokens: 1024, messages: [['role' => 'user', 'content' => 'Generate text until you say END']], model: 'claude-opus-5', stopSequences: ['END', 'STOP'], ); if ($response->stopReason === 'stop_sequence') { echo "Stopped at sequence: {$response->stopSequence}", PHP_EOL; } ``` ```ruby Ruby client = Anthropic::Client.new response = client.messages.create( model: "claude-opus-5", max_tokens: 1024, stop_sequences: ["END", "STOP"], messages: [{ role: "user", content: "Generate text until you say END" }] ) if response.stop_reason == :stop_sequence puts "Stopped at sequence: #{response.stop_sequence}" end ``` ### tool\_use Claude is calling a tool and expects you to run it. For most tool use implementations, use the [tool runner](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-runner), which automatically handles tool execution, result formatting, and conversation management. ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "tools": [{ "name": "get_weather", "description": "Get the current weather in a given location", "input_schema": { "type": "object", "properties": {"location": {"type": "string", "description": "City and state"}}, "required": ["location"] } }], "messages": [{"role": "user", "content": "What is the weather in San Francisco?"}] }' | jq '.stop_reason, (.content[] | select(.type == "tool_use"))' ``` ```bash CLI ant messages create --format json <<'YAML' | jq '.stop_reason, (.content[] | select(.type == "tool_use"))' model: claude-opus-5 max_tokens: 1024 messages: - role: user content: What is the weather in San Francisco? tools: - name: get_weather description: Get the current weather in a given location input_schema: type: object properties: location: {type: string, description: City and state} required: [location] YAML ``` ```python Python client = anthropic.Anthropic() weather_tool = { "name": "get_weather", "description": "Get the current weather in a given location", "input_schema": { "type": "object", "properties": { "location": {"type": "string", "description": "City and state"}, }, "required": ["location"], }, } def execute_tool(name, tool_input): """Execute a tool and return the result.""" return f"Weather in {tool_input.get('location', 'unknown')}: 72°F" response = client.messages.create( model="claude-opus-5", max_tokens=1024, tools=[weather_tool], messages=[{"role": "user", "content": "What is the weather in San Francisco?"}], ) if response.stop_reason == "tool_use": # Extract and execute the tool for block in response.content: if block.type == "tool_use": result = execute_tool(block.name, block.input) # Return result to Claude for final response ``` ```typescript TypeScript const client = new Anthropic(); const weatherTool: Anthropic.Tool = { name: "get_weather", description: "Get the current weather in a given location", input_schema: { type: "object", properties: { location: { type: "string", description: "City and state" } }, required: ["location"] } }; function executeTool(name: string, input: Record): string { return `Weather in ${input.location ?? "unknown"}: 72°F`; } const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, tools: [weatherTool], messages: [{ role: "user", content: "What is the weather in San Francisco?" }] }); if (response.stop_reason === "tool_use") { // Extract and execute the tool for (const block of response.content) { if (block.type === "tool_use") { const result = executeTool(block.name, block.input as Record); // Return result to Claude for final response } } } ``` ```csharp C# AnthropicClient client = new(); var weatherTool = new Tool { Name = "get_weather", Description = "Get the current weather in a given location", InputSchema = new InputSchema { Properties = new Dictionary { ["location"] = JsonSerializer.SerializeToElement( new { type = "string", description = "City and state" } ), }, Required = ["location"] } }; var response = await client.Messages.Create(new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Tools = [weatherTool], Messages = [new() { Role = Role.User, Content = "What is the weather in San Francisco?" }] }); if (response.StopReason == "tool_use") { // Extract and execute the tool foreach (var block in response.Content) { if (block.TryPickToolUse(out var toolUse)) { // Execute toolUse.Name with toolUse.Input and return the result to Claude } } } ``` ```go Go client := anthropic.NewClient() weatherTool := anthropic.ToolParam{ Name: "get_weather", Description: anthropic.String("Get the current weather in a given location"), InputSchema: anthropic.ToolInputSchemaParam{ Properties: map[string]any{ "location": map[string]string{"type": "string", "description": "City and state"}, }, Required: []string{"location"}, }, } response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Tools: []anthropic.ToolUnionParam{{OfTool: &weatherTool}}, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("What is the weather in San Francisco?")), }, }) if err != nil { log.Fatal(err) } if response.StopReason == "tool_use" { // Extract and execute the tool for _, block := range response.Content { if toolUse, ok := block.AsAny().(anthropic.ToolUseBlock); ok { fmt.Println(toolUse.Name, toolUse.Input) // Return result to Claude for final response } } } ``` ```java Java void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); Tool weatherTool = Tool.builder() .name("get_weather") .description("Get the current weather in a given location") .inputSchema(Tool.InputSchema.builder() .properties(JsonValue.from(Map.of( "location", Map.of("type", "string", "description", "City and state") ))) .putAdditionalProperty("required", JsonValue.from(List.of("location"))) .build()) .build(); Message response = client.messages().create( MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addTool(weatherTool) .addUserMessage("What is the weather in San Francisco?") .build() ); if (response.stopReason().map(StopReason.TOOL_USE::equals).orElse(false)) { // Extract and execute the tool for (ContentBlock block : response.content()) { block.toolUse().ifPresent(toolUse -> { // Execute toolUse.name() with toolUse.input() and return the result to Claude }); } } ``` ```php PHP $client = new Client(); $weatherTool = [ 'name' => 'get_weather', 'description' => 'Get the current weather in a given location', 'input_schema' => [ 'type' => 'object', 'properties' => [ 'location' => ['type' => 'string', 'description' => 'City and state'], ], 'required' => ['location'], ], ]; $response = $client->messages->create( maxTokens: 1024, messages: [['role' => 'user', 'content' => 'What is the weather in San Francisco?']], model: 'claude-opus-5', tools: [$weatherTool], ); if ($response->stopReason === 'tool_use') { // Extract and execute the tool foreach ($response->content as $block) { if ($block->type === 'tool_use') { // Execute $block->name with $block->input and return the result to Claude } } } ``` ```ruby Ruby client = Anthropic::Client.new weather_tool = { name: "get_weather", description: "Get the current weather in a given location", input_schema: { type: "object", properties: { location: { type: "string", description: "City and state" } }, required: ["location"] } } response = client.messages.create( model: "claude-opus-5", max_tokens: 1024, tools: [weather_tool], messages: [{ role: "user", content: "What is the weather in San Francisco?" }] ) if response.stop_reason == :tool_use # Extract and execute the tool response.content.each do |block| next unless block.type == :tool_use # Execute block.name with block.input and return the result to Claude end end ``` A `tool_use` response can also contain a `server_tool_use` block whose `id` has no matching result block. That server tool call is not finished, and this response does not carry its result. In the common case, Claude calls a [server tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/server-tools) and one of your client tools in the same group of parallel tool calls: the API returns without running the server tool so that you can run the client tools first. There is no other marker for the state; detect it by checking each `server_tool_use` or `mcp_tool_use` block's `id` for a matching result block. With [programmatic tool calling](https://platform.claude.com/docs/en/agents-and-tools/tool-use/programmatic-tool-calling), the same response shape means something different. The client `tool_use` block comes from code that is running in the `code_execution` tool rather than from Claude directly, and its `caller` field names the `code_execution` block that called it. That code has already started: it is paused waiting for your `tool_result` blocks, and sending them resumes the execution instead of starting a deferred tool. The `code_execution` block's own result block arrives once the code finishes, which can take more than one round of tool results. The follow-up user message itself is the same in both cases; with programmatic tool calling, also pass back the `id` from the response's `container` field, as that page shows. ```json A mixed tool_use response { "stop_reason": "tool_use", "content": [ { "type": "server_tool_use", "id": "srvtoolu_01HxbWnMRmbWyMfUtJKC45rA", "name": "web_search", "input": { "query": "example article" } }, { "type": "tool_use", "id": "toolu_01PjgRJLbXrXEMZwDNYLnBqk", "name": "run_command", "input": { "command": "uname -a" } } ] } ``` The continuation is a user message of `tool_result` blocks, one for every `tool_use` block in the response (see [Handle tool calls](https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls)), with two extra rules: that message must contain nothing except the `tool_result` blocks, and the request must keep the same `tools` array. A resume request that no longer defines the waiting server tool fails with a 400 whose message ends ``but no `web_search` tool was provided``. The API attaches your results to the still-open assistant turn, runs the deferred server tool (for paused code execution, resumes it), and continues the turn. For a server tool Claude called directly, the next response's `content` starts with the result block that answers the previous response's `server_tool_use` `id`. ```json The follow-up user message { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_01PjgRJLbXrXEMZwDNYLnBqk", "content": "Linux demo-host 6.8.0-52-generic x86_64 GNU/Linux" } ] } ``` Adding anything after the `tool_result` blocks in that user message, such as text, ends the assistant turn; for a server tool Claude called directly, the request then fails with a 400 `invalid_request_error` that names the unresolved server tool: ```text wrap `web_search` tool use with id `srvtoolu_01HxbWnMRmbWyMfUtJKC45rA` was found without a corresponding `web_search_tool_result` block ``` Leaving out a `tool_result`, or putting one after other content, fails earlier with the standard `tool_use ids were found without tool_result blocks immediately after` error instead. To give Claude more input, send it as a separate user message after the turn completes. ### pause\_turn Returned when the server-side sampling loop reaches its iteration limit while executing [server tools](https://platform.claude.com/docs/en/agents-and-tools/tool-use/server-tools) such as web search. The default limit is 10 iterations per request. When this happens, the response may contain a `server_tool_use` block without a corresponding result block. To let Claude finish processing, continue the conversation by sending the response back as-is. A response that leaves a client `tool_use` block waiting on you never has a `stop_reason` of `pause_turn`: when Claude stops to call your tools, `stop_reason` is [`tool_use`](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons#tool-use), and you continue it by sending the client `tool_result` blocks instead of the response itself. ```bash cURL # The SDKs handle continuation directly. With cURL, inspect stop_reason # on the response and re-POST with the assistant content appended. curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 4096, "tools": [{"type": "web_search_20250305", "name": "web_search"}], "messages": [{"role": "user", "content": "Search for latest AI news"}] }' | jq '{stop_reason, content}' ``` ```bash CLI # Inspect stop_reason; if it is pause_turn, re-run with the assistant # response appended to --message. ant messages create --format json <<'YAML' | jq '{stop_reason, content}' model: claude-opus-5 max_tokens: 4096 tools: - {type: web_search_20250305, name: web_search} messages: - {role: user, content: "Search for latest AI news"} YAML ``` ```python Python response = client.messages.create( model="claude-opus-5", max_tokens=4096, tools=[{"type": "web_search_20250305", "name": "web_search"}], messages=[{"role": "user", "content": "Search for latest AI news"}], ) if response.stop_reason == "pause_turn": # Continue the conversation by sending the response back messages = [ {"role": "user", "content": "Search for latest AI news"}, {"role": "assistant", "content": response.content}, ] continuation = client.messages.create( model="claude-opus-5", max_tokens=4096, messages=messages, tools=[{"type": "web_search_20250305", "name": "web_search"}], ) ``` ```typescript TypeScript const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 4096, tools: [{ type: "web_search_20250305", name: "web_search" }], messages: [{ role: "user", content: "Search for latest AI news" }] }); if (response.stop_reason === "pause_turn") { // Continue the conversation by sending the response back const continuation = await client.messages.create({ model: "claude-opus-5", max_tokens: 4096, tools: [{ type: "web_search_20250305", name: "web_search" }], messages: [ { role: "user", content: "Search for latest AI news" }, { role: "assistant", content: response.content } ] }); } ``` ```csharp C# List tools = [new ToolUnion(new WebSearchTool20250305())]; MessageParam userMessage = new() { Role = Role.User, Content = "Search for latest AI news" }; var response = await client.Messages.Create(new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 4096, Tools = tools, Messages = [userMessage] }); if (response.StopReason == "pause_turn") { // Continue the conversation by sending the response back var continuation = await client.Messages.Create(new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 4096, Tools = tools, Messages = [ userMessage, new() { Role = Role.Assistant, Content = response.Content.Select(block => new ContentBlockParam(block.Json)).ToList() } ] }); } ``` ```go Go tools := []anthropic.ToolUnionParam{ {OfWebSearchTool20250305: &anthropic.WebSearchTool20250305Param{}}, } userMessage := anthropic.NewUserMessage(anthropic.NewTextBlock("Search for latest AI news")) response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 4096, Tools: tools, Messages: []anthropic.MessageParam{userMessage}, }) if err != nil { log.Fatal(err) } if response.StopReason == "pause_turn" { // Continue the conversation by sending the response back var contentParams []anthropic.ContentBlockParamUnion for _, block := range response.Content { contentParams = append(contentParams, block.ToParam()) } continuation, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 4096, Tools: tools, Messages: []anthropic.MessageParam{userMessage, anthropic.NewAssistantMessage(contentParams...)}, }) if err != nil { log.Fatal(err) } _ = continuation } ``` ```java Java Message response = client.messages().create( MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(4096L) .addTool(WebSearchTool20250305.builder().build()) .addUserMessage("Search for latest AI news") .build() ); if (response.stopReason().map(StopReason.PAUSE_TURN::equals).orElse(false)) { // Continue the conversation by sending the response back Message continuation = client.messages().create( MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(4096L) .addTool(WebSearchTool20250305.builder().build()) .addUserMessage("Search for latest AI news") .addMessage(response) .build() ); } ``` ```php PHP $tools = [['type' => 'web_search_20250305', 'name' => 'web_search']]; $userMessage = ['role' => 'user', 'content' => 'Search for latest AI news']; $response = $client->messages->create( maxTokens: 4096, messages: [$userMessage], model: 'claude-opus-5', tools: $tools, ); if ($response->stopReason === 'pause_turn') { // Continue the conversation by sending the response back $continuation = $client->messages->create( maxTokens: 4096, messages: [ $userMessage, ['role' => 'assistant', 'content' => $response->content], ], model: 'claude-opus-5', tools: $tools, ); } ``` ```ruby Ruby tools = [{ type: "web_search_20250305", name: "web_search" }] user_message = { role: "user", content: "Search for latest AI news" } response = client.messages.create( model: "claude-opus-5", max_tokens: 4096, tools: tools, messages: [user_message] ) if response.stop_reason == :pause_turn # Continue the conversation by sending the response back continuation = client.messages.create( model: "claude-opus-5", max_tokens: 4096, tools: tools, messages: [user_message, { role: "assistant", content: response.content }] ) end ``` Your application should handle `pause_turn` in any agent loop that uses server tools. Add the assistant's response to your messages array and make another API request to let Claude continue. ### refusal Claude declined to generate a response. Safety classifiers return this stop reason as a normal HTTP 200 response, not an error. ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "messages": [{"role": "user", "content": "[Unsafe request]"}] }' | jq '{stop_reason, stop_details}' ``` ```bash CLI ant messages create \ --model claude-opus-5 \ --max-tokens 1024 \ --message '{role: user, content: "[Unsafe request]"}' \ --format json | jq '{stop_reason, stop_details}' ``` ```python Python client = anthropic.Anthropic() response = client.messages.create( model="claude-opus-5", max_tokens=1024, messages=[{"role": "user", "content": "[Unsafe request]"}], ) if response.stop_reason == "refusal": # Claude declined to respond print("Claude was unable to process this request") # Consider rephrasing or modifying the request ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, messages: [{ role: "user", content: "[Unsafe request]" }] }); if (response.stop_reason === "refusal") { // Claude declined to respond console.log("Claude was unable to process this request"); // Consider rephrasing or modifying the request } ``` ```csharp C# AnthropicClient client = new(); var response = await client.Messages.Create(new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [new() { Role = Role.User, Content = "[Unsafe request]" }] }); if (response.StopReason == "refusal") { // Claude declined to respond Console.WriteLine("Claude was unable to process this request"); // Consider rephrasing or modifying the request } ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("[Unsafe request]")), }, }) if err != nil { log.Fatal(err) } if response.StopReason == "refusal" { // Claude declined to respond fmt.Println("Claude was unable to process this request") // Consider rephrasing or modifying the request } ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); Message response = client.messages().create( MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addUserMessage("[Unsafe request]") .build() ); if (response.stopReason().map(StopReason.REFUSAL::equals).orElse(false)) { // Claude declined to respond IO.println("Claude was unable to process this request"); // Consider rephrasing or modifying the request } ``` ```php PHP $client = new Client(); $response = $client->messages->create( maxTokens: 1024, messages: [['role' => 'user', 'content' => '[Unsafe request]']], model: 'claude-opus-5', ); if ($response->stopReason === 'refusal') { // Claude declined to respond echo 'Claude was unable to process this request', PHP_EOL; // Consider rephrasing or modifying the request } ``` ```ruby Ruby client = Anthropic::Client.new response = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [{ role: "user", content: "[Unsafe request]" }] ) if response.stop_reason == :refusal # Claude declined to respond puts "Claude was unable to process this request" # Consider rephrasing or modifying the request end ``` If you encounter `refusal` stop reasons frequently while using Claude Sonnet 4.5 or Claude Opus 4.1 (the latter [retired, except on Bedrock and Google Cloud](https://platform.claude.com/docs/en/about-claude/model-deprecations)), you can try updating your API calls to use Haiku 4.5 (`claude-haiku-4-5-20251001`), which has different usage restrictions. Learn more about [understanding Sonnet 4.5's API safety filters](https://support.claude.com/en/articles/12449294-understanding-sonnet-4-5-s-api-safety-filters). On a refusal, the `stop_details` object identifies the policy category that triggered it. The categories and the full refusal response shape are covered on [Refusals and fallback](https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback#refusal-response). `stop_details` is `null` for all stop reasons other than `refusal`. A refused request on Claude Fable 5 or Claude Opus 5 can usually be served by retrying on another Claude model, and [Refusals and fallback](https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback) shows how to set up that retry, server-side or in your client. [Fallback credit](https://platform.claude.com/docs/en/build-with-claude/fallback-credit) covers how to avoid paying the prompt-cache cost twice when you build the retry yourself. ### model\_context\_window\_exceeded Claude stopped because it reached the model's context window limit. This lets you request the maximum possible tokens without knowing the exact input size. This stop reason is currently typed only in the SDKs' `beta` namespace, so the following examples call `client.beta.messages` and use the `Beta`-prefixed types. On Sonnet 4.5 and newer models the API returns this value without a beta header. For earlier models, add the `model-context-window-exceeded-2025-08-26` beta header to enable it. ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 20000, "messages": [{"role": "user", "content": "Large input that uses most of context window..."}] }' | jq '.stop_reason' ``` ```bash CLI ant messages create \ --model claude-opus-5 \ --max-tokens 20000 \ --message '{role: user, content: "Large input that uses most of context window..."}' \ --format json | jq '.stop_reason' ``` ```python Python # Request with maximum tokens to get as much as possible response = client.beta.messages.create( model="claude-opus-5", max_tokens=20000, # Python SDK requires streaming for max_tokens above ~21k messages=[ {"role": "user", "content": "Large input that uses most of context window..."} ], ) if response.stop_reason == "model_context_window_exceeded": # Response hit context window limit before max_tokens print("Response reached model's context window limit") # The response is still valid but was limited by context window ``` ```typescript TypeScript // Request with maximum tokens to get as much as possible const response = await client.beta.messages.create({ model: "claude-opus-5", max_tokens: 20000, messages: [{ role: "user", content: "Large input that uses most of context window..." }] }); if (response.stop_reason === "model_context_window_exceeded") { // Response hit context window limit before max_tokens console.log("Response reached model's context window limit"); // The response is still valid but was limited by context window } ``` ```csharp C# using Anthropic.Models.Beta.Messages; using Model = Anthropic.Models.Messages.Model; // Request with maximum tokens to get as much as possible var response = await client.Beta.Messages.Create(new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 20000, Messages = [new() { Role = Role.User, Content = "Large input that uses most of context window..." }] }); if (response.StopReason?.Value() == BetaStopReason.ModelContextWindowExceeded) { // Response hit context window limit before max_tokens Console.WriteLine("Response reached model's context window limit"); // The response is still valid but was limited by context window } ``` ```go Go // Request with maximum tokens to get as much as possible response, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 20000, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Large input that uses most of context window...")), }, }) if err != nil { log.Fatal(err) } if response.StopReason == anthropic.BetaStopReasonModelContextWindowExceeded { // Response hit context window limit before max_tokens fmt.Println("Response reached model's context window limit") // The response is still valid but was limited by context window } ``` ```java Java import com.anthropic.models.beta.messages.BetaMessage; import com.anthropic.models.beta.messages.BetaStopReason; import com.anthropic.models.beta.messages.MessageCreateParams; // Request with maximum tokens to get as much as possible BetaMessage response = client.beta().messages().create( MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(20000L) .addUserMessage("Large input that uses most of context window...") .build() ); if (response.stopReason().map(BetaStopReason.MODEL_CONTEXT_WINDOW_EXCEEDED::equals).orElse(false)) { // Response hit context window limit before max_tokens IO.println("Response reached model's context window limit"); // The response is still valid but was limited by context window } ``` ```php PHP // Request with maximum tokens to get as much as possible $response = $client->beta->messages->create( maxTokens: 20000, messages: [['role' => 'user', 'content' => 'Large input that uses most of context window...']], model: 'claude-opus-5', ); if ($response->stopReason === 'model_context_window_exceeded') { // Response hit context window limit before max_tokens echo 'Response reached model\'s context window limit', PHP_EOL; // The response is still valid but was limited by context window } ``` ```ruby Ruby # Request with maximum tokens to get as much as possible response = client.beta.messages.create( model: "claude-opus-5", max_tokens: 20000, messages: [{ role: "user", content: "Large input that uses most of context window..." }] ) if response.stop_reason == :model_context_window_exceeded # Response hit context window limit before max_tokens puts "Response reached model's context window limit" # The response is still valid but was limited by context window end ``` ## Best practices for handling stop reasons ### Always check stop\_reason Make it a habit to check the `stop_reason` in your response handling logic: ```python Python def handle_response(response): if response.stop_reason == "tool_use": return handle_tool_use(response) elif response.stop_reason == "max_tokens": return handle_truncation(response) elif response.stop_reason == "model_context_window_exceeded": return handle_context_limit(response) elif response.stop_reason == "pause_turn": return handle_pause(response) elif response.stop_reason == "refusal": return handle_refusal(response) else: # Handle end_turn and other cases return next( (block.text for block in response.content if block.type == "text"), "" ) ``` ```typescript TypeScript function handleResponse(response: Anthropic.Beta.BetaMessage): string { switch (response.stop_reason) { case "tool_use": return handleToolUse(response); case "max_tokens": return handleTruncation(response); case "model_context_window_exceeded": return handleContextLimit(response); case "pause_turn": return handlePause(response); case "refusal": return handleRefusal(response); default: { // Handle end_turn and other cases const textBlock = response.content.find( (block): block is Anthropic.Beta.BetaTextBlock => block.type === "text" ); return textBlock?.text ?? ""; } } } ``` ```csharp C# static string HandleResponse(BetaMessage response) { return response.StopReason?.Value() switch { BetaStopReason.ToolUse => HandleToolUse(response), BetaStopReason.MaxTokens => HandleTruncation(response), BetaStopReason.ModelContextWindowExceeded => HandleContextLimit(response), BetaStopReason.PauseTurn => HandlePause(response), BetaStopReason.Refusal => HandleRefusal(response), // Handle end_turn and other cases _ => response.Content.Select(b => b.Value).OfType().FirstOrDefault()?.Text ?? "", }; } ``` ```go Go func handleResponse(response *anthropic.BetaMessage) string { switch response.StopReason { case anthropic.BetaStopReasonToolUse: return handleToolUse(response) case anthropic.BetaStopReasonMaxTokens: return handleTruncation(response) case anthropic.BetaStopReasonModelContextWindowExceeded: return handleContextLimit(response) case anthropic.BetaStopReasonPauseTurn: return handlePause(response) case anthropic.BetaStopReasonRefusal: return handleRefusal(response) default: // Handle end_turn and other cases for _, block := range response.Content { if textBlock, ok := block.AsAny().(anthropic.BetaTextBlock); ok { return textBlock.Text } } return "" } } ``` ```java Java static String handleResponse(BetaMessage response) { BetaStopReason reason = response.stopReason().orElse(BetaStopReason.END_TURN); if (reason.equals(BetaStopReason.TOOL_USE)) { return handleToolUse(response); } else if (reason.equals(BetaStopReason.MAX_TOKENS)) { return handleTruncation(response); } else if (reason.equals(BetaStopReason.MODEL_CONTEXT_WINDOW_EXCEEDED)) { return handleContextLimit(response); } else if (reason.equals(BetaStopReason.PAUSE_TURN)) { return handlePause(response); } else if (reason.equals(BetaStopReason.REFUSAL)) { return handleRefusal(response); } // Handle end_turn and other cases return response.content().stream() .filter(BetaContentBlock::isText) .findFirst() .map(block -> block.asText().text()) .orElse(""); } ``` ```php PHP function handle_response($response): string { return match ($response->stopReason) { 'tool_use' => handle_tool_use($response), 'max_tokens' => handle_truncation($response), 'model_context_window_exceeded' => handle_context_limit($response), 'pause_turn' => handle_pause($response), 'refusal' => handle_refusal($response), // Handle end_turn and other cases default => array_find($response->content, static fn ($block): bool => $block->type === 'text')?->text ?? '', }; } ``` ```ruby Ruby def handle_response(response) case response.stop_reason when :tool_use then handle_tool_use(response) when :max_tokens then handle_truncation(response) when :model_context_window_exceeded then handle_context_limit(response) when :pause_turn then handle_pause(response) when :refusal then handle_refusal(response) else # Handle end_turn and other cases response.content.find { it.type == :text }&.text end end ``` ### Handle truncated responses gracefully When a response is truncated because of token limits or the context window, append a notice so the reader knows the output is incomplete. To continue generating from where the response left off instead, see [Ensuring complete responses](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons#ensuring-complete-responses). ```python Python def handle_truncated_response(response): text = next((block.text for block in response.content if block.type == "text"), "") if response.stop_reason in ["max_tokens", "model_context_window_exceeded"]: if response.stop_reason == "max_tokens": note = "[Response truncated due to max_tokens limit]" else: note = "[Response truncated due to context window limit]" return f"{text}\n\n{note}" return text ``` ```typescript TypeScript function handleTruncatedResponse(response: Anthropic.Beta.BetaMessage): string { const textBlock = response.content.find( (block): block is Anthropic.Beta.BetaTextBlock => block.type === "text" ); const text = textBlock?.text ?? ""; if ( response.stop_reason === "max_tokens" || response.stop_reason === "model_context_window_exceeded" ) { const note = response.stop_reason === "max_tokens" ? "[Response truncated due to max_tokens limit]" : "[Response truncated due to context window limit]"; return `${text}\n\n${note}`; } return text; } ``` ```csharp C# static string HandleTruncatedResponse(BetaMessage response) { var text = response.Content.Select(b => b.Value).OfType().FirstOrDefault()?.Text ?? ""; var reason = response.StopReason?.Value(); if (reason is BetaStopReason.MaxTokens or BetaStopReason.ModelContextWindowExceeded) { var note = reason == BetaStopReason.MaxTokens ? "[Response truncated due to max_tokens limit]" : "[Response truncated due to context window limit]"; return $"{text}\n\n{note}"; } return text; } ``` ```go Go func handleTruncatedResponse(response *anthropic.BetaMessage) string { text := "" for _, block := range response.Content { if textBlock, ok := block.AsAny().(anthropic.BetaTextBlock); ok { text = textBlock.Text break } } if response.StopReason == anthropic.BetaStopReasonMaxTokens || response.StopReason == anthropic.BetaStopReasonModelContextWindowExceeded { note := "[Response truncated due to context window limit]" if response.StopReason == anthropic.BetaStopReasonMaxTokens { note = "[Response truncated due to max_tokens limit]" } return text + "\n\n" + note } return text } ``` ```java Java static String handleTruncatedResponse(BetaMessage response) { String text = response.content().stream() .filter(BetaContentBlock::isText) .findFirst() .map(block -> block.asText().text()) .orElse(""); BetaStopReason reason = response.stopReason().orElse(BetaStopReason.END_TURN); if (reason.equals(BetaStopReason.MAX_TOKENS) || reason.equals(BetaStopReason.MODEL_CONTEXT_WINDOW_EXCEEDED)) { String note = reason.equals(BetaStopReason.MAX_TOKENS) ? "[Response truncated due to max_tokens limit]" : "[Response truncated due to context window limit]"; return text + "\n\n" + note; } return text; } ``` ```php PHP function handle_truncated_response($response): string { $text = array_find($response->content, static fn ($block): bool => $block->type === 'text')?->text ?? ''; if (in_array($response->stopReason, ['max_tokens', 'model_context_window_exceeded'], true)) { $note = $response->stopReason === 'max_tokens' ? '[Response truncated due to max_tokens limit]' : '[Response truncated due to context window limit]'; return "{$text}\n\n{$note}"; } return $text; } ``` ```ruby Ruby def handle_truncated_response(response) text = response.content.find { it.type == :text }&.text if [:max_tokens, :model_context_window_exceeded].include?(response.stop_reason) note = if response.stop_reason == :max_tokens "[Response truncated due to max_tokens limit]" else "[Response truncated due to context window limit]" end return "#{text}\n\n#{note}" end text end ``` ### Implement retry logic for pause\_turn When using [server tools](https://platform.claude.com/docs/en/agents-and-tools/tool-use/server-tools), the API may return `pause_turn` if the server-side sampling loop reaches its iteration limit (default 10). Handle this by continuing the conversation: ```python Python def handle_server_tool_conversation(client, user_query, tools, max_continuations=5): """ Handle server tool conversations that may require multiple continuations. The server runs a sampling loop when executing server tools. If the loop reaches its iteration limit, the API returns pause_turn. Continue the conversation by sending the response back to let Claude finish. """ messages = [{"role": "user", "content": user_query}] for _ in range(max_continuations): response = client.messages.create( model="claude-opus-5", max_tokens=4096, messages=messages, tools=tools ) if response.stop_reason != "pause_turn": # Claude finished processing - return the final response return response # pause_turn: replace the full message list to maintain alternating roles messages = [ {"role": "user", "content": user_query}, {"role": "assistant", "content": response.content}, ] # Reached max continuations - return the last response return response ``` ```typescript TypeScript async function handleServerToolConversation( client: Anthropic, userQuery: string, tools: Anthropic.ToolUnion[], maxContinuations = 5 ): Promise { let messages: Anthropic.MessageParam[] = [{ role: "user", content: userQuery }]; let response: Anthropic.Message; for (let i = 0; i < maxContinuations; i++) { response = await client.messages.create({ model: "claude-opus-5", max_tokens: 4096, messages, tools }); if (response.stop_reason !== "pause_turn") { // Claude finished processing - return the final response return response; } // pause_turn: replace the full message list to maintain alternating roles messages = [ { role: "user", content: userQuery }, { role: "assistant", content: response.content } ]; } // Reached max continuations - return the last response return response!; } ``` ```csharp C# static async Task HandleServerToolConversation( AnthropicClient client, string userQuery, List tools, int maxContinuations = 5) { List messages = [new() { Role = Role.User, Content = userQuery }]; Message response = null!; for (var i = 0; i < maxContinuations; i++) { response = await client.Messages.Create(new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 4096, Messages = messages, Tools = tools }); if (response.StopReason != "pause_turn") { // Claude finished processing - return the final response return response; } // pause_turn: replace the full message list to maintain alternating roles messages = [ new() { Role = Role.User, Content = userQuery }, new() { Role = Role.Assistant, Content = response.Content.Select(block => new ContentBlockParam(block.Json)).ToList() } ]; } // Reached max continuations - return the last response return response; } ``` ```go Go func handleServerToolConversation( client anthropic.Client, userQuery string, tools []anthropic.ToolUnionParam, maxContinuations int, ) (*anthropic.Message, error) { messages := []anthropic.MessageParam{anthropic.NewUserMessage(anthropic.NewTextBlock(userQuery))} var response *anthropic.Message var err error for range maxContinuations { response, err = client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 4096, Messages: messages, Tools: tools, }) if err != nil { return nil, err } if response.StopReason != "pause_turn" { // Claude finished processing - return the final response return response, nil } // pause_turn: replace the full message list to maintain alternating roles var contentParams []anthropic.ContentBlockParamUnion for _, block := range response.Content { contentParams = append(contentParams, block.ToParam()) } messages = []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock(userQuery)), anthropic.NewAssistantMessage(contentParams...), } } // Reached max continuations - return the last response return response, nil } ``` ```java Java static Message handleServerToolConversation( AnthropicClient client, String userQuery, List tools, int maxContinuations ) { Message response = null; for (int i = 0; i < maxContinuations; i++) { // Rebuild the params each iteration so messages aren't accumulated MessageCreateParams.Builder params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(4096L) .addUserMessage(userQuery); tools.forEach(params::addTool); if (response != null) { params.addMessage(response); } response = client.messages().create(params.build()); if (!response.stopReason().map(StopReason.PAUSE_TURN::equals).orElse(false)) { // Claude finished processing - return the final response return response; } // pause_turn: loop again and send the response back } // Reached max continuations - return the last response return response; } ``` ```php PHP function handle_server_tool_conversation( Client $client, string $userQuery, array $tools, int $maxContinuations = 5 ) { $messages = [['role' => 'user', 'content' => $userQuery]]; $response = null; for ($i = 0; $i < $maxContinuations; $i++) { $response = $client->messages->create( maxTokens: 4096, messages: $messages, model: 'claude-opus-5', tools: $tools, ); if ($response->stopReason !== 'pause_turn') { // Claude finished processing - return the final response return $response; } // pause_turn: replace the full message list to maintain alternating roles $messages = [ ['role' => 'user', 'content' => $userQuery], ['role' => 'assistant', 'content' => $response->content], ]; } // Reached max continuations - return the last response return $response; } ``` ```ruby Ruby def handle_server_tool_conversation(client, user_query, tools, max_continuations: 5) messages = [{ role: "user", content: user_query }] response = nil max_continuations.times do response = client.messages.create( model: "claude-opus-5", max_tokens: 4096, messages: messages, tools: tools ) # Claude finished processing - return the final response return response unless response.stop_reason == :pause_turn # pause_turn: replace the full message list to maintain alternating roles messages = [ { role: "user", content: user_query }, { role: "assistant", content: response.content } ] end # Reached max continuations - return the last response response end ``` ## Stop reasons vs. errors It's important to distinguish between `stop_reason` values and actual errors: ### Stop reasons (successful responses) * Part of the response body * Indicate why generation stopped normally * Response contains valid content ### Errors (failed requests) * HTTP status codes 4xx or 5xx * Indicate request processing failures * Response contains error details ```bash cURL # cURL exits non-zero on HTTP errors with --fail-with-body; inspect # $? for errors and stop_reason for successful responses. curl --fail-with-body -sS https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "messages": [{"role": "user", "content": "Hello!"}] }' | jq '.stop_reason' ``` ```bash CLI # The CLI exits non-zero on API errors; stop_reason appears on success. ant messages create \ --model claude-opus-5 \ --max-tokens 1024 \ --message '{role: user, content: "Hello!"}' \ --format json | jq '.stop_reason' ``` ```python Python client = anthropic.Anthropic() try: response = client.messages.create( model="claude-opus-5", max_tokens=1024, messages=[{"role": "user", "content": "Hello!"}], ) # Handle successful response with stop_reason if response.stop_reason == "max_tokens": print("Response was truncated") except anthropic.APIStatusError as e: # Handle actual errors if e.status_code == 429: print("Rate limit exceeded") elif e.status_code == 500: print("Server error") ``` ```typescript TypeScript const client = new Anthropic(); try { const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, messages: [{ role: "user", content: "Hello!" }] }); // Handle successful response with stop_reason if (response.stop_reason === "max_tokens") { console.log("Response was truncated"); } } catch (err) { // Handle actual errors if (err instanceof Anthropic.APIError) { if (err.status === 429) { console.log("Rate limit exceeded"); } else if (err.status === 500) { console.log("Server error"); } } else { throw err; } } ``` ```csharp C# AnthropicClient client = new(); try { var response = await client.Messages.Create(new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [new() { Role = Role.User, Content = "Hello!" }] }); // Handle successful response with stop_reason if (response.StopReason == "max_tokens") { Console.WriteLine("Response was truncated"); } } catch (AnthropicRateLimitException) { // Handle actual errors Console.WriteLine("Rate limit exceeded"); } catch (Anthropic5xxException) { Console.WriteLine("Server error"); } ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Hello!")), }, }) if err != nil { // Handle actual errors var apiErr *anthropic.Error if errors.As(err, &apiErr) { switch apiErr.StatusCode { case 429: fmt.Println("Rate limit exceeded") case 500: fmt.Println("Server error") } } log.Fatal(err) } // Handle successful response with stop_reason if response.StopReason == "max_tokens" { fmt.Println("Response was truncated") } ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); try { Message response = client.messages().create( MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addUserMessage("Hello!") .build() ); // Handle successful response with stop_reason if (response.stopReason().map(StopReason.MAX_TOKENS::equals).orElse(false)) { IO.println("Response was truncated"); } } catch (RateLimitException e) { // Handle actual errors IO.println("Rate limit exceeded"); } catch (AnthropicServiceException e) { if (e.statusCode() == 500) { IO.println("Server error"); } } ``` ```php PHP $client = new Client(); try { $response = $client->messages->create( maxTokens: 1024, messages: [['role' => 'user', 'content' => 'Hello!']], model: 'claude-opus-5', ); // Handle successful response with stop_reason if ($response->stopReason === 'max_tokens') { echo 'Response was truncated', PHP_EOL; } } catch (RateLimitException $e) { // Handle actual errors echo 'Rate limit exceeded', PHP_EOL; } catch (InternalServerException $e) { echo 'Server error', PHP_EOL; } ``` ```ruby Ruby client = Anthropic::Client.new begin response = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [{ role: "user", content: "Hello!" }] ) # Handle successful response with stop_reason if response.stop_reason == :max_tokens puts "Response was truncated" end rescue Anthropic::Errors::RateLimitError # Handle actual errors puts "Rate limit exceeded" rescue Anthropic::Errors::APIStatusError => e puts "Server error" if e.status == 500 end ``` ## Streaming considerations When using streaming, `stop_reason` is: * `null` in the initial `message_start` event * Provided in the `message_delta` event * Not provided in any other events ```bash cURL # The message_delta event in the SSE stream carries stop_reason. curl --no-buffer https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "stream": true, "messages": [{"role": "user", "content": "Hello!"}] }' ``` ```bash CLI # stop_reason appears in the message_delta event. ant messages create --stream --format jsonl \ --model claude-opus-5 \ --max-tokens 1024 \ --message '{role: user, content: "Hello!"}' | jq -c 'select(.type == "message_delta") | .delta.stop_reason' ``` ```python Python client = anthropic.Anthropic() with client.messages.stream( model="claude-opus-5", max_tokens=1024, messages=[{"role": "user", "content": "Hello!"}], ) as stream: for event in stream: if event.type == "message_delta": stop_reason = event.delta.stop_reason if stop_reason: print(f"Stream ended with: {stop_reason}") ``` ```typescript TypeScript const client = new Anthropic(); const stream = client.messages.stream({ model: "claude-opus-5", max_tokens: 1024, messages: [{ role: "user", content: "Hello!" }] }); for await (const event of stream) { if (event.type === "message_delta" && event.delta.stop_reason) { console.log(`Stream ended with: ${event.delta.stop_reason}`); } } ``` ```csharp C# AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [new() { Role = Role.User, Content = "Hello!" }] }; await foreach (var streamEvent in client.Messages.CreateStreaming(parameters)) { switch (streamEvent.Value) { case RawMessageDeltaEvent deltaEvent when deltaEvent.Delta.StopReason is not null: Console.WriteLine($"Stream ended with: {deltaEvent.Delta.StopReason}"); break; } } ``` ```go Go client := anthropic.NewClient() stream := client.Messages.NewStreaming(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Hello!")), }, }) // Accumulate events into the final Message, which carries stop_reason. message := anthropic.Message{} for stream.Next() { if err := message.Accumulate(stream.Current()); err != nil { log.Fatal(err) } } if err := stream.Err(); err != nil { log.Fatal(err) } if message.StopReason != "" { fmt.Printf("Stream ended with: %s\n", message.StopReason) } ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addUserMessage("Hello!") .build(); // Accumulate events into the final Message, which carries stop_reason. MessageAccumulator accumulator = MessageAccumulator.create(); try (StreamResponse streamResponse = client.messages().createStreaming(params)) { streamResponse.stream().forEach(accumulator::accumulate); } accumulator.message().stopReason().ifPresent(stopReason -> IO.println("Stream ended with: " + stopReason) ); ``` ```php PHP $client = new Client(); $stream = $client->messages->createStream( maxTokens: 1024, messages: [['role' => 'user', 'content' => 'Hello!']], model: 'claude-opus-5', ); foreach ($stream as $event) { if ($event instanceof RawMessageDeltaEvent && $event->delta->stopReason !== null) { echo "Stream ended with: {$event->delta->stopReason}", PHP_EOL; } } ``` ```ruby Ruby client = Anthropic::Client.new stream = client.messages.stream( model: "claude-opus-5", max_tokens: 1024, messages: [{ role: "user", content: "Hello!" }] ) stream.each do |event| next unless event.type == :message_delta stop_reason = event.delta.stop_reason puts "Stream ended with: #{stop_reason}" if stop_reason end ``` ## Common patterns ### Handling tool use workflows **Simpler with tool runner:** The following example shows manual tool handling. For most use cases, the [tool runner](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-runner) automatically handles tool execution with much less code. ```python Python def complete_tool_workflow(client, user_query, tools): messages = [{"role": "user", "content": user_query}] while True: response = client.messages.create( model="claude-opus-5", max_tokens=1024, messages=messages, tools=tools ) if response.stop_reason == "tool_use": # Execute tools and continue tool_results = execute_tools(response.content) messages.append({"role": "assistant", "content": response.content}) messages.append({"role": "user", "content": tool_results}) else: # Final response return response ``` ```typescript TypeScript async function completeToolWorkflow( client: Anthropic, userQuery: string, tools: Anthropic.ToolUnion[] ): Promise { const messages: Anthropic.MessageParam[] = [{ role: "user", content: userQuery }]; while (true) { const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, messages, tools }); if (response.stop_reason === "tool_use") { // Execute tools and continue const toolResults = executeTools(response.content); messages.push({ role: "assistant", content: response.content }); messages.push({ role: "user", content: toolResults }); } else { // Final response return response; } } } ``` ```csharp C# static async Task CompleteToolWorkflow( AnthropicClient client, string userQuery, List tools) { List messages = [new() { Role = Role.User, Content = userQuery }]; while (true) { var response = await client.Messages.Create(new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = messages, Tools = tools }); if (response.StopReason == "tool_use") { // Execute tools and continue var toolResults = ExecuteTools(response.Content); messages.Add(new() { Role = Role.Assistant, Content = response.Content.Select(block => new ContentBlockParam(block.Json)).ToList() }); messages.Add(new() { Role = Role.User, Content = toolResults }); } else { // Final response return response; } } } ``` ```go Go func completeToolWorkflow( client anthropic.Client, userQuery string, tools []anthropic.ToolUnionParam, ) (*anthropic.Message, error) { messages := []anthropic.MessageParam{anthropic.NewUserMessage(anthropic.NewTextBlock(userQuery))} for { response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: messages, Tools: tools, }) if err != nil { return nil, err } if response.StopReason != "tool_use" { // Final response return response, nil } // Execute tools and continue toolResults := executeTools(response.Content) var contentParams []anthropic.ContentBlockParamUnion for _, block := range response.Content { contentParams = append(contentParams, block.ToParam()) } messages = append(messages, anthropic.NewAssistantMessage(contentParams...)) messages = append(messages, anthropic.NewUserMessage(toolResults...)) } } ``` ```java Java static Message completeToolWorkflow( AnthropicClient client, String userQuery, List tools ) { List messages = new ArrayList<>(); messages.add(MessageParam.builder().role(MessageParam.Role.USER).content(userQuery).build()); while (true) { MessageCreateParams.Builder params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .messages(messages); tools.forEach(params::addTool); Message response = client.messages().create(params.build()); if (!response.stopReason().map(StopReason.TOOL_USE::equals).orElse(false)) { // Final response return response; } // Execute tools and continue List toolResults = executeTools(response.content()); messages.add(response.toParam()); messages.add(MessageParam.builder() .role(MessageParam.Role.USER) .contentOfBlockParams(toolResults.stream().map(ContentBlockParam::ofToolResult).toList()) .build()); } } ``` ```php PHP function complete_tool_workflow(Client $client, string $userQuery, array $tools) { $messages = [['role' => 'user', 'content' => $userQuery]]; while (true) { $response = $client->messages->create( maxTokens: 1024, messages: $messages, model: 'claude-opus-5', tools: $tools, ); if ($response->stopReason !== 'tool_use') { // Final response return $response; } // Execute tools and continue $toolResults = execute_tools($response->content); $messages[] = ['role' => 'assistant', 'content' => $response->content]; $messages[] = ['role' => 'user', 'content' => $toolResults]; } } ``` ```ruby Ruby def complete_tool_workflow(client, user_query, tools) messages = [{ role: "user", content: user_query }] loop do response = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: messages, tools: tools ) # Final response return response unless response.stop_reason == :tool_use # Execute tools and continue tool_results = execute_tools(response.content) messages << { role: "assistant", content: response.content } messages << { role: "user", content: tool_results } end end ``` ### Ensuring complete responses ```python Python def get_complete_response(client, prompt, max_attempts=3): messages = [{"role": "user", "content": prompt}] full_response = "" for _ in range(max_attempts): response = client.messages.create( model="claude-opus-5", messages=messages, max_tokens=4096 ) full_response += next( (block.text for block in response.content if block.type == "text"), "" ) if response.stop_reason != "max_tokens": break # Continue from where it left off messages = [ {"role": "user", "content": prompt}, {"role": "assistant", "content": full_response}, {"role": "user", "content": "Please continue from where you left off."}, ] return full_response ``` ```typescript TypeScript async function getCompleteResponse( client: Anthropic, prompt: string, maxAttempts = 3 ): Promise { let messages: Anthropic.MessageParam[] = [{ role: "user", content: prompt }]; let fullResponse = ""; for (let i = 0; i < maxAttempts; i++) { const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 4096, messages }); const textBlock = response.content.find( (block): block is Anthropic.TextBlock => block.type === "text" ); fullResponse += textBlock?.text ?? ""; if (response.stop_reason !== "max_tokens") { break; } // Continue from where it left off messages = [ { role: "user", content: prompt }, { role: "assistant", content: fullResponse }, { role: "user", content: "Please continue from where you left off." } ]; } return fullResponse; } ``` ```csharp C# static async Task GetCompleteResponse(AnthropicClient client, string prompt, int maxAttempts = 3) { List messages = [new() { Role = Role.User, Content = prompt }]; var fullResponse = ""; for (var i = 0; i < maxAttempts; i++) { var response = await client.Messages.Create(new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 4096, Messages = messages }); foreach (var block in response.Content) { if (block.TryPickText(out var textBlock)) { fullResponse += textBlock.Text; break; } } if (response.StopReason != "max_tokens") { break; } // Continue from where it left off messages = [ new() { Role = Role.User, Content = prompt }, new() { Role = Role.Assistant, Content = fullResponse }, new() { Role = Role.User, Content = "Please continue from where you left off." } ]; } return fullResponse; } ``` ```go Go func getCompleteResponse(client anthropic.Client, prompt string, maxAttempts int) (string, error) { messages := []anthropic.MessageParam{anthropic.NewUserMessage(anthropic.NewTextBlock(prompt))} fullResponse := "" for range maxAttempts { response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 4096, Messages: messages, }) if err != nil { return "", err } for _, block := range response.Content { if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok { fullResponse += textBlock.Text break } } if response.StopReason != "max_tokens" { break } // Continue from where it left off messages = []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock(prompt)), anthropic.NewAssistantMessage(anthropic.NewTextBlock(fullResponse)), anthropic.NewUserMessage(anthropic.NewTextBlock("Please continue from where you left off.")), } } return fullResponse, nil } ``` ```java Java static String getCompleteResponse(AnthropicClient client, String prompt, int maxAttempts) { List messages = List.of( MessageParam.builder().role(MessageParam.Role.USER).content(prompt).build() ); StringBuilder fullResponse = new StringBuilder(); for (int i = 0; i < maxAttempts; i++) { Message response = client.messages().create( MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(4096L) .messages(messages) .build() ); response.content().stream() .filter(ContentBlock::isText) .findFirst() .ifPresent(block -> fullResponse.append(block.asText().text())); if (!response.stopReason().map(StopReason.MAX_TOKENS::equals).orElse(false)) { break; } // Continue from where it left off messages = List.of( MessageParam.builder().role(MessageParam.Role.USER).content(prompt).build(), MessageParam.builder().role(MessageParam.Role.ASSISTANT).content(fullResponse.toString()).build(), MessageParam.builder().role(MessageParam.Role.USER).content("Please continue from where you left off.").build() ); } return fullResponse.toString(); } ``` ```php PHP function get_complete_response(Client $client, string $prompt, int $maxAttempts = 3): string { $messages = [['role' => 'user', 'content' => $prompt]]; $fullResponse = ''; for ($i = 0; $i < $maxAttempts; $i++) { $response = $client->messages->create( maxTokens: 4096, messages: $messages, model: 'claude-opus-5', ); $fullResponse .= array_find($response->content, static fn ($block): bool => $block->type === 'text')?->text ?? ''; if ($response->stopReason !== 'max_tokens') { break; } // Continue from where it left off $messages = [ ['role' => 'user', 'content' => $prompt], ['role' => 'assistant', 'content' => $fullResponse], ['role' => 'user', 'content' => 'Please continue from where you left off.'], ]; } return $fullResponse; } ``` ```ruby Ruby def get_complete_response(client, prompt, max_attempts: 3) messages = [{ role: "user", content: prompt }] full_response = +"" max_attempts.times do response = client.messages.create( model: "claude-opus-5", max_tokens: 4096, messages: messages ) full_response << response.content.find { it.type == :text }&.text.to_s break unless response.stop_reason == :max_tokens # Continue from where it left off messages = [ { role: "user", content: prompt }, { role: "assistant", content: full_response }, { role: "user", content: "Please continue from where you left off." } ] end full_response end ``` ### Getting maximum tokens without knowing input size With the `model_context_window_exceeded` stop reason, you can request the maximum possible tokens without calculating input size: ```python Python def get_max_possible_tokens(client, prompt): """ Get as many tokens as possible within the model's context window without needing to calculate input token count """ response = client.beta.messages.create( model="claude-opus-5", messages=[{"role": "user", "content": prompt}], max_tokens=20000, # Python SDK requires streaming for max_tokens above ~21k ) if response.stop_reason == "model_context_window_exceeded": # Got the maximum possible tokens given input size print( f"Generated {response.usage.output_tokens} tokens (context limit reached)" ) elif response.stop_reason == "max_tokens": # Got exactly the requested tokens print(f"Generated {response.usage.output_tokens} tokens (max_tokens reached)") else: # Natural completion print(f"Generated {response.usage.output_tokens} tokens (natural completion)") return next((block.text for block in response.content if block.type == "text"), "") ``` ```typescript TypeScript async function getMaxPossibleTokens(client: Anthropic, prompt: string): Promise { const response = await client.beta.messages.create({ model: "claude-opus-5", max_tokens: 20000, messages: [{ role: "user", content: prompt }] }); const tokens = response.usage.output_tokens; if (response.stop_reason === "model_context_window_exceeded") { // Got the maximum possible tokens given input size console.log(`Generated ${tokens} tokens (context limit reached)`); } else if (response.stop_reason === "max_tokens") { // Got exactly the requested tokens console.log(`Generated ${tokens} tokens (max_tokens reached)`); } else { // Natural completion console.log(`Generated ${tokens} tokens (natural completion)`); } const textBlock = response.content.find( (block): block is Anthropic.Beta.BetaTextBlock => block.type === "text" ); return textBlock?.text ?? ""; } ``` ```csharp C# using Anthropic.Models.Beta.Messages; using Model = Anthropic.Models.Messages.Model; static async Task GetMaxPossibleTokens(AnthropicClient client, string prompt) { var response = await client.Beta.Messages.Create(new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 20000, Messages = [new() { Role = Role.User, Content = prompt }] }); var tokens = response.Usage.OutputTokens; var reason = response.StopReason?.Value(); if (reason == BetaStopReason.ModelContextWindowExceeded) { // Got the maximum possible tokens given input size Console.WriteLine($"Generated {tokens} tokens (context limit reached)"); } else if (reason == BetaStopReason.MaxTokens) { // Got exactly the requested tokens Console.WriteLine($"Generated {tokens} tokens (max_tokens reached)"); } else { // Natural completion Console.WriteLine($"Generated {tokens} tokens (natural completion)"); } return response.Content.Select(b => b.Value).OfType().FirstOrDefault()?.Text ?? ""; } ``` ```go Go func getMaxPossibleTokens(client anthropic.Client, prompt string) (string, error) { response, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 20000, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock(prompt)), }, }) if err != nil { return "", err } tokens := response.Usage.OutputTokens switch response.StopReason { case anthropic.BetaStopReasonModelContextWindowExceeded: // Got the maximum possible tokens given input size fmt.Printf("Generated %d tokens (context limit reached)\n", tokens) case anthropic.BetaStopReasonMaxTokens: // Got exactly the requested tokens fmt.Printf("Generated %d tokens (max_tokens reached)\n", tokens) default: // Natural completion fmt.Printf("Generated %d tokens (natural completion)\n", tokens) } for _, block := range response.Content { if textBlock, ok := block.AsAny().(anthropic.BetaTextBlock); ok { return textBlock.Text, nil } } return "", nil } ``` ```java Java import com.anthropic.models.beta.messages.BetaContentBlock; import com.anthropic.models.beta.messages.BetaMessage; import com.anthropic.models.beta.messages.BetaStopReason; import com.anthropic.models.beta.messages.MessageCreateParams; static String getMaxPossibleTokens(AnthropicClient client, String prompt) { BetaMessage response = client.beta().messages().create( MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(20000L) .addUserMessage(prompt) .build() ); long tokens = response.usage().outputTokens(); BetaStopReason reason = response.stopReason().orElse(BetaStopReason.END_TURN); if (reason.equals(BetaStopReason.MODEL_CONTEXT_WINDOW_EXCEEDED)) { // Got the maximum possible tokens given input size IO.println("Generated " + tokens + " tokens (context limit reached)"); } else if (reason.equals(BetaStopReason.MAX_TOKENS)) { // Got exactly the requested tokens IO.println("Generated " + tokens + " tokens (max_tokens reached)"); } else { // Natural completion IO.println("Generated " + tokens + " tokens (natural completion)"); } return response.content().stream() .filter(BetaContentBlock::isText) .findFirst() .map(block -> block.asText().text()) .orElse(""); } ``` ```php PHP function get_max_possible_tokens(Client $client, string $prompt): string { $response = $client->beta->messages->create( maxTokens: 20000, messages: [['role' => 'user', 'content' => $prompt]], model: 'claude-opus-5', ); $tokens = $response->usage->outputTokens; echo match ($response->stopReason) { // Got the maximum possible tokens given input size 'model_context_window_exceeded' => "Generated {$tokens} tokens (context limit reached)", // Got exactly the requested tokens 'max_tokens' => "Generated {$tokens} tokens (max_tokens reached)", // Natural completion default => "Generated {$tokens} tokens (natural completion)", }, PHP_EOL; return array_find($response->content, static fn ($block): bool => $block->type === 'text')?->text ?? ''; } ``` ```ruby Ruby def get_max_possible_tokens(client, prompt) response = client.beta.messages.create( model: "claude-opus-5", max_tokens: 20000, messages: [{ role: "user", content: prompt }] ) tokens = response.usage.output_tokens case response.stop_reason when :model_context_window_exceeded # Got the maximum possible tokens given input size puts "Generated #{tokens} tokens (context limit reached)" when :max_tokens # Got exactly the requested tokens puts "Generated #{tokens} tokens (max_tokens reached)" else # Natural completion puts "Generated #{tokens} tokens (natural completion)" end response.content.find { it.type == :text }.text end ``` ## Next steps Retry refused requests on a fallback model, server-side or in your client. Let the SDK manage the `tool_use` loop, result formatting, and retries for you. Read `stop_reason` from the `message_delta` event when streaming. Handle 4xx and 5xx HTTP errors, which are distinct from stop reasons. --- title: Using the Messages API url: https://platform.claude.com/docs/en/build-with-claude/working-with-messages description: Practical patterns and examples for using the Messages API effectively --- Anthropic offers two ways to build with Claude, each suited to different use cases: | | Messages API | Claude Managed Agents | | -------------- | ------------------------------------------- | ------------------------------------------------------------------------- | | **What it is** | Direct model prompting access | Pre-built, configurable agent harness that runs in managed infrastructure | | **Best for** | Custom agent loops and fine-grained control | Long-running tasks and asynchronous work | This guide covers common patterns for working with the Messages API, including basic requests, multi-turn conversations, prefill techniques, and vision capabilities. For complete API specifications, see the [Messages API reference](https://platform.claude.com/docs/en/api/messages/create). For the managed agent harness instead, see the [Claude Managed Agents overview](https://platform.claude.com/docs/en/managed-agents/overview). For how zero data retention (ZDR) applies to this feature, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention). ## Basic request and response The `temperature`, `top_p`, and `top_k` sampling parameters are not supported on Claude 4.7 and later models and Claude Mythos Preview. Setting them to a non-default value returns a 400 error. Omit them from request payloads and use prompting to guide the model's behavior instead. See the [migration guide](https://platform.claude.com/docs/en/about-claude/models/migration-guide#migrating-from-claude-opus-47). ```bash cURL #!/bin/sh curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "messages": [ {"role": "user", "content": "Hello, Claude"} ] }' ``` ```bash CLI ant messages create \ --model claude-opus-5 \ --max-tokens 1024 \ --message '{role: user, content: "Hello, Claude"}' ``` ```python Python message = anthropic.Anthropic().messages.create( model="claude-opus-5", max_tokens=1024, messages=[{"role": "user", "content": "Hello, Claude"}], ) print(message) ``` ```typescript TypeScript const anthropic = new Anthropic(); const message = await anthropic.messages.create({ model: "claude-opus-5", max_tokens: 1024, messages: [{ role: "user", content: "Hello, Claude" }] }); console.log(message); ``` ```csharp C# AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [new() { Role = Role.User, Content = "Hello, Claude" }] }; var message = await client.Messages.Create(parameters); Console.WriteLine(message); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Hello, Claude")), }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addUserMessage("Hello, Claude") .build(); Message response = client.messages().create(params); System.out.println(response); ``` ```php PHP $client = new Client(); $message = $client->messages->create( maxTokens: 1024, messages: [['role' => 'user', 'content' => 'Hello, Claude']], model: 'claude-opus-5', ); echo json_encode($message, JSON_PRETTY_PRINT), PHP_EOL; ``` ```ruby Ruby client = Anthropic::Client.new message = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: "Hello, Claude" } ] ) puts message ``` ```json Output { "id": "msg_01XFDUDYJgAACzvnptvVoYEL", "type": "message", "role": "assistant", "content": [ { "type": "text", "text": "Hello!" } ], "model": "claude-opus-5", "stop_reason": "end_turn", "stop_sequence": null, "usage": { "input_tokens": 12, "output_tokens": 6 } } ``` Refusal responses (`stop_reason: "refusal"`) also include a `stop_details` object identifying the policy category that triggered the refusal, on every model. See [Handling stop reasons](https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback#refusal-response) for the field reference and example handling code. ## Multiple conversational turns The Messages API is stateless, which means that you always send the full conversational history to the API. You can use this pattern to build up a conversation over time. Earlier conversational turns don't necessarily need to actually originate from Claude. You can use synthetic `assistant` messages. ```bash cURL #!/bin/sh curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "messages": [ {"role": "user", "content": "Hello, Claude"}, {"role": "assistant", "content": "Hello!"}, {"role": "user", "content": "Can you describe LLMs to me?"} ] }' ``` ```bash CLI ant messages create \ --model claude-opus-5 \ --max-tokens 1024 \ --message '{role: user, content: "Hello, Claude"}' \ --message '{role: assistant, content: "Hello!"}' \ --message '{role: user, content: "Can you describe LLMs to me?"}' ``` ```python Python message = anthropic.Anthropic().messages.create( model="claude-opus-5", max_tokens=1024, messages=[ {"role": "user", "content": "Hello, Claude"}, {"role": "assistant", "content": "Hello!"}, {"role": "user", "content": "Can you describe LLMs to me?"}, ], ) print(message) ``` ```typescript TypeScript const anthropic = new Anthropic(); const message = await anthropic.messages.create({ model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: "Hello, Claude" }, { role: "assistant", content: "Hello!" }, { role: "user", content: "Can you describe LLMs to me?" } ] }); console.log(message); ``` ```csharp C# AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [ new() { Role = Role.User, Content = "Hello, Claude" }, new() { Role = Role.Assistant, Content = "Hello!" }, new() { Role = Role.User, Content = "Can you describe LLMs to me?" } ] }; var message = await client.Messages.Create(parameters); Console.WriteLine(message); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Hello, Claude")), anthropic.NewAssistantMessage(anthropic.NewTextBlock("Hello!")), anthropic.NewUserMessage(anthropic.NewTextBlock("Can you describe LLMs to me?")), }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addUserMessage("Hello, Claude") .addAssistantMessage("Hello!") .addUserMessage("Can you describe LLMs to me?") .build(); Message response = client.messages().create(params); System.out.println(response); ``` ```php PHP $client = new Client(); $message = $client->messages->create( maxTokens: 1024, messages: [ ['role' => 'user', 'content' => 'Hello, Claude'], ['role' => 'assistant', 'content' => 'Hello!'], ['role' => 'user', 'content' => 'Can you describe LLMs to me?'], ], model: 'claude-opus-5', ); echo json_encode($message, JSON_PRETTY_PRINT), PHP_EOL; ``` ```ruby Ruby client = Anthropic::Client.new message = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: "Hello, Claude" }, { role: "assistant", content: "Hello!" }, { role: "user", content: "Can you describe LLMs to me?" } ] ) puts message ``` ```json Output { "id": "msg_018gCsTGsXkYJVqYPxTgDHBU", "type": "message", "role": "assistant", "content": [ { "type": "text", "text": "Sure, I'd be happy to provide..." } ], "model": "claude-opus-5", "stop_reason": "end_turn", "stop_sequence": null, "usage": { "input_tokens": 30, "output_tokens": 309 } } ``` ### System role in messages On Claude Fable 5, [Claude Mythos 5](https://anthropic.com/glasswing), Claude Opus 4.8, and Claude Opus 5, you can include messages with `"role": "system"` after a user turn (subject to [placement rules](https://platform.claude.com/docs/en/build-with-claude/mid-conversation-system-messages#limitations)) to add a new system instruction partway through a conversation. A `system` message cannot be the first entry in `messages`; use the top-level `system` field for instructions that apply from the start. A mid-conversation system message has the same authority as the top-level `system` field, but because it is appended to the end of the message history, it does not invalidate any cached prefix that came before it. Use the top-level `system` field for instructions that should apply from the very first turn, and a mid-conversation system message for instructions that only become relevant later. See [Mid-conversation system messages](https://platform.claude.com/docs/en/build-with-claude/mid-conversation-system-messages) for the complete guide, including how to combine it with [prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching). ## Prefilling Claude's response You can pre-fill part of Claude's response in the last position of the input messages list. Use this technique to shape Claude's response. The following example uses `"max_tokens": 1` to get a single multiple choice answer from Claude. Prefilling is not supported on Claude 4.6 and later models and [Claude Mythos Preview](https://anthropic.com/glasswing). Requests using prefill with these models return a 400 error. Use [structured outputs](https://platform.claude.com/docs/en/build-with-claude/structured-outputs) on models that support it, or system prompt instructions, instead. See the [migration guide](https://platform.claude.com/docs/en/about-claude/models/migration-guide) for migration patterns. ```bash cURL #!/bin/sh curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-sonnet-4-5", "max_tokens": 1, "messages": [ {"role": "user", "content": "What is latin for Ant? (A) Apoidea, (B) Rhopalocera, (C) Formicidae"}, {"role": "assistant", "content": "The answer is ("} ] }' ``` ```bash CLI ant messages create <<'YAML' model: claude-sonnet-4-5 max_tokens: 1 messages: - role: user content: "What is latin for Ant? (A) Apoidea, (B) Rhopalocera, (C) Formicidae" - role: assistant content: "The answer is (" YAML ``` ```python Python message = anthropic.Anthropic().messages.create( model="claude-sonnet-4-5", max_tokens=1, messages=[ { "role": "user", "content": "What is latin for Ant? (A) Apoidea, (B) Rhopalocera, (C) Formicidae", }, {"role": "assistant", "content": "The answer is ("}, ], ) print(message) ``` ```typescript TypeScript const anthropic = new Anthropic(); const message = await anthropic.messages.create({ model: "claude-sonnet-4-5", max_tokens: 1, messages: [ { role: "user", content: "What is latin for Ant? (A) Apoidea, (B) Rhopalocera, (C) Formicidae" }, { role: "assistant", content: "The answer is (" } ] }); console.log(message); ``` ```csharp C# AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Model.ClaudeSonnet4_5, MaxTokens = 1, Messages = [ new() { Role = Role.User, Content = "What is latin for Ant? (A) Apoidea, (B) Rhopalocera, (C) Formicidae" }, new() { Role = Role.Assistant, Content = "The answer is (" } ] }; var message = await client.Messages.Create(parameters); Console.WriteLine(message); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeSonnet4_5, MaxTokens: 1, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("What is latin for Ant? (A) Apoidea, (B) Rhopalocera, (C) Formicidae")), anthropic.NewAssistantMessage(anthropic.NewTextBlock("The answer is (")), }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_SONNET_4_5) .maxTokens(1L) .addUserMessage("What is latin for Ant? (A) Apoidea, (B) Rhopalocera, (C) Formicidae") .addAssistantMessage("The answer is (") .build(); Message response = client.messages().create(params); System.out.println(response); ``` ```php PHP $client = new Client(); $message = $client->messages->create( maxTokens: 1, messages: [ ['role' => 'user', 'content' => 'What is latin for Ant? (A) Apoidea, (B) Rhopalocera, (C) Formicidae'], ['role' => 'assistant', 'content' => 'The answer is ('], ], model: 'claude-sonnet-4-5', ); echo $message->content[0]->text; ``` ```ruby Ruby client = Anthropic::Client.new message = client.messages.create( model: "claude-sonnet-4-5", max_tokens: 1, messages: [ { role: "user", content: "What is latin for Ant? (A) Apoidea, (B) Rhopalocera, (C) Formicidae" }, { role: "assistant", content: "The answer is (" } ] ) puts message ``` ```json Output { "id": "msg_01Q8Faay6S7QPTvEUUQARt7h", "type": "message", "role": "assistant", "content": [ { "type": "text", "text": "C" } ], "model": "claude-sonnet-4-5", "stop_reason": "max_tokens", "stop_sequence": null, "usage": { "input_tokens": 42, "output_tokens": 1 } } ``` ## Vision Claude can read both text and images in requests. You can supply images using the `base64`, `url`, or `file` source types. The `file` source type references an image uploaded through the [Files API](https://platform.claude.com/docs/en/build-with-claude/files). Supported media types are `image/jpeg`, `image/png`, `image/gif`, and `image/webp`. See the [vision guide](https://platform.claude.com/docs/en/build-with-claude/vision) for more details. ```bash cURL #!/bin/sh # Option 1: Base64-encoded image IMAGE_URL="https://platform.claude.com/docs/images/vision-example.jpg" IMAGE_MEDIA_TYPE="image/jpeg" IMAGE_BASE64=$(curl "$IMAGE_URL" | base64 | tr -d '\n') curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d @- < { new ContentBlockParam(new ImageBlockParam( new ImageBlockParamSource(new Base64ImageSource() { Data = imageData, MediaType = MediaType.ImageJpeg, }) )), new ContentBlockParam(new TextBlockParam("What is in the above image?")), }), } ] }; var message = await client.Messages.Create(parameters); Console.WriteLine(message); // Option 2: URL-referenced image var parametersFromUrl = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [ new() { Role = Role.User, Content = new MessageParamContent(new List { new ContentBlockParam(new ImageBlockParam( new ImageBlockParamSource(new UrlImageSource() { Url = "https://platform.claude.com/docs/images/vision-example.jpg", }) )), new ContentBlockParam(new TextBlockParam("What is in the above image?")), }), } ] }; var messageFromUrl = await client.Messages.Create(parametersFromUrl); Console.WriteLine(messageFromUrl); ``` ```go Go client := anthropic.NewClient() // Option 1: Base64-encoded image imageURL := "https://platform.claude.com/docs/images/vision-example.jpg" req, err := http.NewRequest("GET", imageURL, nil) if err != nil { log.Fatal(err) } req.Header.Set("User-Agent", "AnthropicDocsBot/1.0") resp, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } defer resp.Body.Close() imageBytes, err := io.ReadAll(resp.Body) if err != nil { log.Fatal(err) } imageData := base64.StdEncoding.EncodeToString(imageBytes) message, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage( anthropic.NewImageBlockBase64("image/jpeg", imageData), anthropic.NewTextBlock("What is in the above image?"), ), }, }) if err != nil { log.Fatal(err) } fmt.Println(message) // Option 2: URL-referenced image messageFromURL, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage( anthropic.NewImageBlock(anthropic.URLImageSourceParam{ URL: "https://platform.claude.com/docs/images/vision-example.jpg", }), anthropic.NewTextBlock("What is in the above image?"), ), }, }) if err != nil { log.Fatal(err) } fmt.Println(messageFromURL) ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); // Option 1: Base64-encoded image String imageUrl = "https://platform.claude.com/docs/images/vision-example.jpg"; HttpClient httpClient = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder().uri(URI.create(imageUrl)).build(); HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofByteArray()); String imageData = Base64.getEncoder().encodeToString(response.body()); List base64Content = List.of( ContentBlockParam.ofImage( ImageBlockParam.builder() .source(Base64ImageSource.builder() .data(imageData) .mediaType(Base64ImageSource.MediaType.IMAGE_JPEG) .build()) .build()), ContentBlockParam.ofText( TextBlockParam.builder() .text("What is in the above image?") .build()) ); Message message = client.messages().create( MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addUserMessageOfBlockParams(base64Content) .build()); System.out.println(message); // Option 2: URL-referenced image List urlContent = List.of( ContentBlockParam.ofImage( ImageBlockParam.builder() .source(UrlImageSource.builder() .url("https://platform.claude.com/docs/images/vision-example.jpg") .build()) .build()), ContentBlockParam.ofText( TextBlockParam.builder() .text("What is in the above image?") .build()) ); Message messageFromUrl = client.messages().create( MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addUserMessageOfBlockParams(urlContent) .build()); System.out.println(messageFromUrl); ``` ```php PHP $client = new Client(); // Option 1: Base64-encoded image $image_url = 'https://platform.claude.com/docs/images/vision-example.jpg'; $image_media_type = "image/jpeg"; $image_data = base64_encode(file_get_contents($image_url)); $message = $client->messages->create( maxTokens: 1024, messages: [ [ 'role' => 'user', 'content' => [ [ 'type' => 'image', 'source' => [ 'type' => 'base64', 'media_type' => $image_media_type, 'data' => $image_data, ], ], [ 'type' => 'text', 'text' => 'What is in the above image?', ], ], ], ], model: 'claude-opus-5', ); echo $message; // Option 2: URL-referenced image $message_from_url = $client->messages->create( maxTokens: 1024, messages: [ [ 'role' => 'user', 'content' => [ [ 'type' => 'image', 'source' => [ 'type' => 'url', 'url' => 'https://platform.claude.com/docs/images/vision-example.jpg', ], ], [ 'type' => 'text', 'text' => 'What is in the above image?', ], ], ], ], model: 'claude-opus-5', ); echo $message_from_url; ``` ```ruby Ruby require "base64" require "net/http" client = Anthropic::Client.new # Option 1: Base64-encoded image image_url = "https://platform.claude.com/docs/images/vision-example.jpg" image_media_type = "image/jpeg" image_data = Base64.strict_encode64(Net::HTTP.get(URI(image_url))) message = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: [ { type: "image", source: { type: "base64", media_type: image_media_type, data: image_data } }, { type: "text", text: "What is in the above image?" } ] } ] ) puts message # Option 2: URL-referenced image message_from_url = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: [ { type: "image", source: { type: "url", url: "https://platform.claude.com/docs/images/vision-example.jpg" } }, { type: "text", text: "What is in the above image?" } ] } ] ) puts message_from_url ``` ```json Output { "id": "msg_011CdKmWtV3oFx1C5yUbf5CY", "type": "message", "role": "assistant", "content": [ { "type": "text", "text": "This image is a beautiful minimalist/flat-design illustration of a sunset landscape. Here's what it contains:\n\n**Sky & Sun:**\n- A warm gradient sky transitioning from golden-yellow at the top to deep orange toward the horizon\n- A large pale yellow sun positioned in the upper-right area\n\n**Birds:**\n- Three small silhouetted birds flying in the upper-left portion of the sky, depicted as simple \"M\" or \"v\" shapes\n\n**Mountains:**\n- Multiple layered mountain peaks in purple and maroon tones\n- The mountains overlap to create depth, with varying shades of dusty purple and deep burgundy\n\n**Water:**\n- A dark purple body of water at the bottom of the image\n- A reflection of the sun shown as horizontal cream/peach colored lines in the center-bottom area\n\nThe overall style is clean, geometric, and uses a warm sunset color palette (oranges, yellows, purples, and maroons), giving it a peaceful, serene aesthetic typical of modern vector/flat design artwork." } ], "model": "claude-opus-5", "stop_reason": "end_turn", "stop_sequence": null, "usage": { "input_tokens": 1030, "output_tokens": 350 } } ``` ## Next steps Handle each `stop_reason` value and decide what to do when a response ends. Give Claude tools to call external services and APIs from within the Messages API. Control desktop computer environments with the Messages API. Get guaranteed, schema-validated JSON output from Claude. Set an advisory token budget across a full agentic loop with `output_config.task_budget`. ### Model capabilities --- title: Batch processing url: https://platform.claude.com/docs/en/build-with-claude/batch-processing description: Process large volumes of Messages requests asynchronously with the Message Batches API, cutting costs by 50% and increasing throughput. --- Batch processing is a powerful approach for handling large volumes of requests efficiently. Instead of processing requests one at a time with immediate responses, batch processing allows you to submit multiple requests together for asynchronous processing. This pattern is particularly useful when: * You need to process large volumes of data * Immediate responses are not required * You want to optimize for cost efficiency * You're running large-scale evaluations or analyses The Message Batches API is Anthropic's first implementation of this pattern. For how zero data retention (ZDR) applies to this feature, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention). # Message Batches API The Message Batches API is a powerful, cost-effective way to asynchronously process large volumes of [Messages](https://platform.claude.com/docs/en/api/messages/create) requests. This approach is well-suited to tasks that do not require immediate responses, with most batches finishing in less than 1 hour while reducing costs by 50% and increasing throughput. You can [explore the API reference directly](https://platform.claude.com/docs/en/api/messages/batches/create), in addition to this guide. ## How the Message Batches API works When you send a request to the Message Batches API: 1. The system creates a new Message Batch with the provided Messages requests. 2. The batch is then processed asynchronously, with each request handled independently. 3. You can poll for the status of the batch and retrieve results when processing has ended for all requests. This is especially useful for bulk operations that don't require immediate results, such as: * Large-scale evaluations: Process thousands of test cases efficiently. * Content moderation: Analyze large volumes of user-generated content asynchronously. * Data analysis: Generate insights or summaries for large datasets. * Bulk content generation: Create large amounts of text for various purposes (for example, product descriptions, article summaries). ### Batch limitations * A Message Batch is limited to either 100,000 Message requests or 256 MB in size, whichever is reached first. * The system processes each batch as fast as possible, with most batches completing within 1 hour. You can access batch results when all messages have completed or after 24 hours, whichever comes first. Batches expire if processing does not complete within 24 hours. * Batch results are available for 29 days after creation. After that, you may still view the Batch, but its results will no longer be available for download. * Batches are scoped to a [Workspace](https://platform.claude.com/settings/workspaces). You may view all batches (and their results) that were created within the Workspace that your API key belongs to. * Rate limits apply to both Batches API HTTP requests and the number of requests within a batch waiting to be processed. See [Message Batches API rate limits](https://platform.claude.com/docs/en/api/rate-limits#message-batches-api). Additionally, processing may be slowed down based on current demand and your request volume. In that case, you may see more requests expiring after 24 hours. * Because of high throughput and concurrent processing, batches may go slightly over your Workspace's configured [spend limit](https://platform.claude.com/settings/billing). * Each batched request must have `max_tokens` of at least `1`. `max_tokens: 0` ([cache pre-warming](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#pre-warming-the-cache)) is not supported inside a batch, because an ephemeral cache entry written during batch processing would likely expire before the follow-up request runs. ### Supported models All [active models](https://platform.claude.com/docs/en/about-claude/models/overview) support the Message Batches API. ### What can be batched Almost any request you can make to the Messages API can be included in a batch. This includes: * Vision * Tool use, including all [server tools](https://platform.claude.com/docs/en/agents-and-tools/tool-use/server-tools) (web search, web fetch, code execution, MCP connectors, advisor, and tool search) * System messages * Multi-turn conversations * Extended thinking * Most beta features Because each request in the batch is processed independently, you can mix different types of requests within a single batch. A small number of Messages API parameters are **not** supported in batch requests. Including any of these returns a validation error: | Parameter | Why | | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | `stream: true` | Batch results come back as a single file, not a stream. | | `speed` ([Fast mode](https://platform.claude.com/docs/en/build-with-claude/fast-mode)) | Fast mode tunes synchronous latency, which doesn't apply to asynchronous batch processing. | | `store` / `previous_thread_event_id` (Threads) | Threads are stateful; batch requests are not. | | `cache_hint` / `context_hint` | These routing hints apply to synchronous request scheduling only. | | `max_tokens: 0` | See [Batch limitations](https://platform.claude.com/docs/en/build-with-claude/batch-processing#batch-limitations). | | `research_preview_2026_02: "active"` | Research preview mode is not available on the batch path. | Because batches can take longer than 5 minutes to process, consider using the [1-hour cache duration](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#1-hour-cache-duration) with prompt caching for better cache hit rates when processing batches with shared context. ## Pricing The Batches API offers significant cost savings. All usage is charged at 50% of the standard API prices. | Model | Batch input | Batch output | | ------------------------------------------------------------------------------------------------------------------------------------- | ------------ | ------------- | | Claude Fable 5 | $5 / MTok | $25 / MTok | | Claude Mythos 5 ([limited availability](https://anthropic.com/glasswing)) | $5 / MTok | $25 / MTok | | Claude Opus 5 | $2.50 / MTok | $12.50 / MTok | | Claude Opus 4.8 | $2.50 / MTok | $12.50 / MTok | | Claude Opus 4.7 | $2.50 / MTok | $12.50 / MTok | | Claude Opus 4.6 | $2.50 / MTok | $12.50 / MTok | | Claude Opus 4.5 | $2.50 / MTok | $12.50 / MTok | | Claude Opus 4.1 ([retired, except on Bedrock and Google Cloud](https://platform.claude.com/docs/en/about-claude/model-deprecations)) | $7.50 / MTok | $37.50 / MTok | | Claude Opus 4 ([retired, except on Google Cloud](https://platform.claude.com/docs/en/about-claude/model-deprecations)) | $7.50 / MTok | $37.50 / MTok | | Claude Sonnet 5 | $1 / MTok | $5 / MTok | | Claude Sonnet 4.6 | $1.50 / MTok | $7.50 / MTok | | Claude Sonnet 4.5 | $1.50 / MTok | $7.50 / MTok | | Claude Sonnet 4 ([retired, except on Bedrock and Google Cloud](https://platform.claude.com/docs/en/about-claude/model-deprecations)) | $1.50 / MTok | $7.50 / MTok | | Claude Haiku 4.5 | $0.50 / MTok | $2.50 / MTok | | Claude Haiku 3.5 ([retired, except on Bedrock and Google Cloud](https://platform.claude.com/docs/en/about-claude/model-deprecations)) | $0.40 / MTok | $2 / MTok | ## How to use the Message Batches API ### Prepare and create your batch A Message Batch is composed of a list of requests to create a Message. The shape of an individual request comprises: * A unique `custom_id` for identifying the Messages request. Must be 1 to 64 characters and contain only alphanumeric characters, hyphens, and underscores (matching `^[a-zA-Z0-9_-]{1,64}$`). * A `params` object with the standard [Messages API](https://platform.claude.com/docs/en/api/messages/create) parameters You can [create a batch](https://platform.claude.com/docs/en/api/messages/batches/create) by passing this list into the `requests` parameter: ```bash cURL curl https://api.anthropic.com/v1/messages/batches \ --header "x-api-key: $ANTHROPIC_API_KEY" \ --header "anthropic-version: 2023-06-01" \ --header "content-type: application/json" \ --data \ '{ "requests": [ { "custom_id": "my-first-request", "params": { "model": "claude-opus-5", "max_tokens": 1024, "messages": [ {"role": "user", "content": "Hello, world"} ] } }, { "custom_id": "my-second-request", "params": { "model": "claude-opus-5", "max_tokens": 1024, "messages": [ {"role": "user", "content": "Hi again, friend"} ] } } ] }' ``` ```bash CLI ant messages:batches create <<'YAML' requests: - custom_id: my-first-request params: model: claude-opus-5 max_tokens: 1024 messages: - role: user content: Hello, world - custom_id: my-second-request params: model: claude-opus-5 max_tokens: 1024 messages: - role: user content: Hi again, friend YAML ``` ```python Python from anthropic.types.message_create_params import MessageCreateParamsNonStreaming from anthropic.types.messages.batch_create_params import Request client = anthropic.Anthropic() message_batch = client.messages.batches.create( requests=[ Request( custom_id="my-first-request", params=MessageCreateParamsNonStreaming( model="claude-opus-5", max_tokens=1024, messages=[ { "role": "user", "content": "Hello, world", } ], ), ), Request( custom_id="my-second-request", params=MessageCreateParamsNonStreaming( model="claude-opus-5", max_tokens=1024, messages=[ { "role": "user", "content": "Hi again, friend", } ], ), ), ] ) print(message_batch) ``` ```typescript TypeScript const client = new Anthropic(); const messageBatch = await client.messages.batches.create({ requests: [ { custom_id: "my-first-request", params: { model: "claude-opus-5", max_tokens: 1024, messages: [{ role: "user", content: "Hello, world" }] } }, { custom_id: "my-second-request", params: { model: "claude-opus-5", max_tokens: 1024, messages: [{ role: "user", content: "Hi again, friend" }] } } ] }); console.log(messageBatch); ``` ```csharp C# using Anthropic; using Anthropic.Models.Messages; using Anthropic.Models.Messages.Batches; AnthropicClient client = new(); var batch = await client.Messages.Batches.Create(new BatchCreateParams { Requests = [ new() { CustomID = "my-first-request", Params = new() { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [ new() { Role = Role.User, Content = "Hello, world" } ] } }, new() { CustomID = "my-second-request", Params = new() { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [ new() { Role = Role.User, Content = "Hi again, friend" } ] } } ] }); Console.WriteLine(batch); ``` ```go Go client := anthropic.NewClient() batch, _ := client.Messages.Batches.New(context.Background(), anthropic.MessageBatchNewParams{ Requests: []anthropic.MessageBatchNewParamsRequest{ { CustomID: "my-first-request", Params: anthropic.MessageBatchNewParamsRequestParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage( anthropic.NewTextBlock("Hello, world"), ), }, }, }, { CustomID: "my-second-request", Params: anthropic.MessageBatchNewParamsRequestParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage( anthropic.NewTextBlock("Hi again, friend"), ), }, }, }, }, }) fmt.Println(batch.ID) ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); BatchCreateParams params = BatchCreateParams.builder() .addRequest( BatchCreateParams.Request.builder() .customId("my-first-request") .params( BatchCreateParams.Request.Params.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .addUserMessage("Hello, world") .build() ) .build() ) .addRequest( BatchCreateParams.Request.builder() .customId("my-second-request") .params( BatchCreateParams.Request.Params.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .addUserMessage("Hi again, friend") .build() ) .build() ) .build(); MessageBatch messageBatch = client.messages().batches().create(params); System.out.println(messageBatch); ``` ```php PHP $client = new Client(); $batch = $client->messages->batches->create( requests: [ [ 'custom_id' => 'my-first-request', 'params' => [ 'model' => 'claude-opus-5', 'max_tokens' => 1024, 'messages' => [ ['role' => 'user', 'content' => 'Hello, world'] ] ] ], [ 'custom_id' => 'my-second-request', 'params' => [ 'model' => 'claude-opus-5', 'max_tokens' => 1024, 'messages' => [ ['role' => 'user', 'content' => 'Hi again, friend'] ] ] ] ], ); echo $batch->id; ``` ```ruby Ruby client = Anthropic::Client.new batch = client.messages.batches.create( requests: [ { custom_id: "my-first-request", params: { model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: "Hello, world" } ] } }, { custom_id: "my-second-request", params: { model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: "Hi again, friend" } ] } } ] ) puts batch ``` In this example, two separate requests are batched together for asynchronous processing. Each request has a unique `custom_id` and contains the standard parameters you'd use for a Messages API call. **Test your batch requests with the Messages API** Validation of the `params` object for each message request is performed asynchronously, and validation errors are returned when processing of the entire batch has ended. You can ensure that you are building your input correctly by verifying your request shape with the [Messages API](https://platform.claude.com/docs/en/api/messages/create) first. When a batch is first created, the response has a processing status of `in_progress`. ```json Output { "id": "msgbatch_01HkcTjaV5uDC8jWR4ZsDV8d", "type": "message_batch", "processing_status": "in_progress", "request_counts": { "processing": 2, "succeeded": 0, "errored": 0, "canceled": 0, "expired": 0 }, "ended_at": null, "created_at": "2024-09-24T18:37:24.100435Z", "expires_at": "2024-09-25T18:37:24.100435Z", "cancel_initiated_at": null, "results_url": null } ``` ### Tracking your batch The Message Batch's `processing_status` field indicates the stage of processing the batch is in. It starts as `in_progress`, then updates to `ended` once all the requests in the batch have finished processing, and results are ready. You can monitor the state of your batch by visiting the [Console](https://platform.claude.com/settings/workspaces/default/batches), or using the [retrieval endpoint](https://platform.claude.com/docs/en/api/retrieving-message-batches). #### Polling for Message Batch completion To poll a Message Batch, you'll need its `id`, which is provided in the response when creating a batch or by listing batches. You can implement a polling loop that checks the batch status periodically until processing has ended: ```bash cURL #!/bin/sh # ... # Check the status; repeat until processing_status is "ended" curl -s "https://api.anthropic.com/v1/messages/batches/$MESSAGE_BATCH_ID" \ --header "x-api-key: $ANTHROPIC_API_KEY" \ --header "anthropic-version: 2023-06-01" \ | jq -r '.processing_status' ``` ```bash CLI #!/bin/bash # ... # Check the status; repeat until processing_status is "ended" ant messages:batches retrieve \ --message-batch-id "$MESSAGE_BATCH_ID" \ --transform processing_status --raw-output ``` ```python Python import time client = anthropic.Anthropic() MESSAGE_BATCH_ID = "msgbatch_01HkcTjaV5uDC8jWR4ZsDV8d" message_batch = None while True: message_batch = client.messages.batches.retrieve(MESSAGE_BATCH_ID) if message_batch.processing_status == "ended": break print(f"Batch {MESSAGE_BATCH_ID} is still processing...") time.sleep(60) print(message_batch) ``` ```typescript TypeScript const client = new Anthropic(); const messageBatchId = "msgbatch_01HkcTjaV5uDC8jWR4ZsDV8d"; let messageBatch; while (true) { messageBatch = await client.messages.batches.retrieve(messageBatchId); if (messageBatch.processing_status === "ended") { break; } console.log(`Batch ${messageBatchId} is still processing... waiting`); await new Promise((resolve) => setTimeout(resolve, 60_000)); } console.log(messageBatch); ``` ```csharp C# AnthropicClient client = new(); string messageBatchId = Environment.GetEnvironmentVariable("MESSAGE_BATCH_ID"); MessageBatch messageBatch = null; while (true) { messageBatch = await client.Messages.Batches.Retrieve(messageBatchId); if (messageBatch.ProcessingStatus == "ended") { break; } Console.WriteLine($"Batch {messageBatchId} is still processing..."); await Task.Delay(60000); } Console.WriteLine(messageBatch); ``` ```go Go client := anthropic.NewClient() messageBatchID := os.Getenv("MESSAGE_BATCH_ID") var messageBatch *anthropic.MessageBatch for { var err error messageBatch, err = client.Messages.Batches.Get(context.TODO(), messageBatchID) if err != nil { log.Fatal(err) } if messageBatch.ProcessingStatus == "ended" { break } fmt.Printf("Batch %s is still processing...\n", messageBatchID) time.Sleep(60 * time.Second) } fmt.Println(messageBatch) ``` ```java Java import com.anthropic.models.messages.batches.MessageBatch; // ... AnthropicClient client = AnthropicOkHttpClient.fromEnv(); String messageBatchId = "msgbatch_01HkcTjaV5uDC8jWR4ZsDV8d"; MessageBatch messageBatch = null; while (true) { messageBatch = client.messages().batches().retrieve(messageBatchId); if (messageBatch.processingStatus().equals(MessageBatch.ProcessingStatus.ENDED)) { break; } System.out.println("Batch " + messageBatchId + " is still processing..."); Thread.sleep(60000); } System.out.println(messageBatch); ``` ```php PHP $client = new Client(); $messageBatchId = getenv("MESSAGE_BATCH_ID"); $messageBatch = null; while (true) { $messageBatch = $client->messages->batches->retrieve( messageBatchID: $messageBatchId, ); if ($messageBatch->processingStatus === "ended") { break; } echo "Batch {$messageBatchId} is still processing...\n"; sleep(60); } echo json_encode($messageBatch, JSON_PRETTY_PRINT); ``` ```ruby Ruby client = Anthropic::Client.new message_batch_id = ENV["MESSAGE_BATCH_ID"] message_batch = nil loop do message_batch = client.messages.batches.retrieve(message_batch_id) break if message_batch.processing_status == :ended puts "Batch #{message_batch_id} is still processing..." sleep 60 end puts message_batch ``` ### Listing all Message Batches You can list all Message Batches in your Workspace using the [list endpoint](https://platform.claude.com/docs/en/api/listing-message-batches). The API supports pagination, automatically fetching additional pages as needed: ```bash cURL #!/bin/sh # Fetches one page. While the response's has_more is true, pass its # last_id as after_id to fetch the next page. (The SDKs and the CLI # perform automatic pagination.) curl -s "https://api.anthropic.com/v1/messages/batches?limit=20" \ --header "x-api-key: $ANTHROPIC_API_KEY" \ --header "anthropic-version: 2023-06-01" ``` ```bash CLI # Automatically fetches more pages as needed ant messages:batches list --limit 20 ``` ```python Python client = anthropic.Anthropic() # Automatically fetches more pages as needed. for message_batch in client.messages.batches.list(limit=20): print(message_batch) ``` ```typescript TypeScript const client = new Anthropic(); // Automatically fetches more pages as needed. for await (const messageBatch of client.messages.batches.list({ limit: 20 })) { console.log(messageBatch); } ``` ```csharp C# AnthropicClient client = new(); var parameters = new BatchListParams { Limit = 20 }; // Automatically fetches more pages as needed var page = await client.Messages.Batches.List(parameters); await foreach (var messageBatch in page.Paginate()) { Console.WriteLine(messageBatch); } ``` ```go Go client := anthropic.NewClient() // Automatically fetches more pages as needed iter := client.Messages.Batches.ListAutoPaging(context.TODO(), anthropic.MessageBatchListParams{ Limit: anthropic.Int(20), }) for iter.Next() { messageBatch := iter.Current() fmt.Println(messageBatch) } if err := iter.Err(); err != nil { log.Fatal(err) } ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); // Automatically fetches more pages as needed for (MessageBatch messageBatch : client .messages() .batches() .list(BatchListParams.builder().limit(20).build()) .autoPager()) { System.out.println(messageBatch); } ``` ```php PHP $client = new Client(); // Automatically fetches more pages as needed foreach ($client->messages->batches->list(limit: 20)->pagingEachItem() as $messageBatch) { echo $messageBatch->id . "\n"; } ``` ```ruby Ruby client = Anthropic::Client.new # Automatically fetches more pages as needed client.messages.batches.list(limit: 20).auto_paging_each do |message_batch| puts message_batch end ``` ### Retrieving batch results Once batch processing has ended, each Messages request in the batch has a result. There are four result types: | Result type | Description | | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `succeeded` | Request was successful. Includes the message result. | | `errored` | Request encountered an error and a message was not created. Possible errors include invalid requests and internal server errors. You will not be billed for these requests. | | `canceled` | User canceled the batch before this request could be sent to the model. You will not be billed for these requests. | | `expired` | Batch reached its 24-hour expiration before this request could be sent to the model. You will not be billed for these requests. | The batch's `request_counts` shows an overview of your results, indicating how many requests reached each of these four states. Results of the batch are available for download at the `results_url` property on the Message Batch, and if the organization permission allows, in the Console. Because of the potentially large size of the results, it's recommended to [stream results](https://platform.claude.com/docs/en/api/messages/batches/results) back rather than download them all at once. ```bash cURL #!/bin/sh # Fetch the batch's results_url, then stream the .jsonl results it # points to. For per-result handling (retries, validation errors), # use the SDK examples in the other tabs. RESULTS_URL=$(curl -s "https://api.anthropic.com/v1/messages/batches/msgbatch_01HkcTjaV5uDC8jWR4ZsDV8d" \ --header "anthropic-version: 2023-06-01" \ --header "x-api-key: $ANTHROPIC_API_KEY" \ | jq -r '.results_url') curl -s "$RESULTS_URL" \ --header "anthropic-version: 2023-06-01" \ --header "x-api-key: $ANTHROPIC_API_KEY" \ | jq -r '"\(.result.type): \(.custom_id)"' ``` ```bash CLI # Prints one line per result, e.g. `{"custom_id":"test-1","type":"succeeded",…}`. # For per-result handling (retries, validation errors), use the SDK # examples in the other tabs. ant messages:batches results \ --message-batch-id msgbatch_01HkcTjaV5uDC8jWR4ZsDV8d \ --transform '{custom_id,"type":result.type,"error":result.error.error.type}' \ --format jsonl ``` ```python Python client = anthropic.Anthropic() # Stream results file in memory-efficient chunks, processing one at a time for result in client.messages.batches.results( "msgbatch_01HkcTjaV5uDC8jWR4ZsDV8d", ): match result.result.type: case "succeeded": print(f"Success! {result.custom_id}") case "errored": if result.result.error.error.type == "invalid_request_error": # Request body must be fixed before re-sending request print(f"Validation error {result.custom_id}") else: # Request can be retried directly print(f"Server error {result.custom_id}") case "expired": print(f"Request expired {result.custom_id}") ``` ```typescript TypeScript const client = new Anthropic(); // Stream results file in memory-efficient chunks, processing one at a time for await (const result of await client.messages.batches.results( "msgbatch_01HkcTjaV5uDC8jWR4ZsDV8d" )) { switch (result.result.type) { case "succeeded": console.log(`Success! ${result.custom_id}`); break; case "errored": if (result.result.error.type === "invalid_request_error") { // Request body must be fixed before re-sending request console.log(`Validation error: ${result.custom_id}`); } else { // Request can be retried directly console.log(`Server error: ${result.custom_id}`); } break; case "expired": console.log(`Request expired: ${result.custom_id}`); break; } } ``` ```csharp C# AnthropicClient client = new(); await foreach (var result in client.Messages.Batches.ResultsStreaming("msgbatch_01HkcTjaV5uDC8jWR4ZsDV8d")) { switch (result.Result.Type) { case "succeeded": Console.WriteLine($"Success! {result.CustomID}"); break; case "errored": if (result.Result.Error?.Type == "invalid_request") { Console.WriteLine($"Validation error: {result.CustomID}"); } else { Console.WriteLine($"Server error: {result.CustomID}"); } break; case "expired": Console.WriteLine($"Request expired: {result.CustomID}"); break; } } ``` ```go Go client := anthropic.NewClient() stream := client.Messages.Batches.ResultsStreaming(context.TODO(), "msgbatch_01HkcTjaV5uDC8jWR4ZsDV8d") for stream.Next() { result := stream.Current() switch variant := result.Result.AsAny().(type) { case anthropic.MessageBatchSucceededResult: fmt.Printf("Success! %s\n", result.CustomID) case anthropic.MessageBatchErroredResult: fmt.Printf("Error: %s - %s\n", result.CustomID, variant.Error.Error.Message) case anthropic.MessageBatchExpiredResult: fmt.Printf("Request expired: %s\n", result.CustomID) } } if err := stream.Err(); err != nil { log.Fatal(err) } ``` ```java Java import com.anthropic.core.http.StreamResponse; import com.anthropic.models.messages.batches.BatchResultsParams; import com.anthropic.models.messages.batches.MessageBatchIndividualResponse; // ... AnthropicClient client = AnthropicOkHttpClient.fromEnv(); // Stream results file in memory-efficient chunks, processing one at a time try ( StreamResponse streamResponse = client .messages() .batches() .resultsStreaming( BatchResultsParams.builder() .messageBatchId("msgbatch_01HkcTjaV5uDC8jWR4ZsDV8d") .build() ) ) { streamResponse .stream() .forEach(result -> { if (result.result().isSucceeded()) { System.out.println("Success! " + result.customId()); } else if (result.result().isErrored()) { if (result.result().asErrored().error().error().isInvalidRequestError()) { // Request body must be fixed before re-sending request System.out.println("Validation error: " + result.customId()); } else { // Request can be retried directly System.out.println("Server error: " + result.customId()); } } else if (result.result().isExpired()) { System.out.println("Request expired: " + result.customId()); } }); } ``` ```php PHP $client = new Client(); foreach ($client->messages->batches->resultsStream(messageBatchID: 'msgbatch_01HkcTjaV5uDC8jWR4ZsDV8d') as $result) { switch ($result->result->type) { case "succeeded": echo "Success! {$result->customID}\n"; break; case "errored": if ($result->result->error->error->type === "invalid_request_error") { echo "Validation error: {$result->customID}\n"; } else { echo "Server error: {$result->customID}\n"; } break; case "expired": echo "Request expired: {$result->customID}\n"; break; } } ``` ```ruby Ruby client = Anthropic::Client.new client.messages.batches.results_streaming("msgbatch_01HkcTjaV5uDC8jWR4ZsDV8d").each do |result| case result.result.type when :succeeded puts "Success! #{result.custom_id}" when :errored if result.result.error.type == :invalid_request puts "Validation error: #{result.custom_id}" else puts "Server error: #{result.custom_id}" end when :expired puts "Request expired: #{result.custom_id}" end end ``` The results are in `.jsonl` format, where each line is a valid JSON object representing the result of a single request in the Message Batch. For each streamed result, you can do something different depending on its `custom_id` and result type. Here is an example set of results: ```jsonl .jsonl file {"custom_id":"my-second-request","result":{"type":"succeeded","message":{"id":"msg_014VwiXbi91y3JMjcpyGBHX5","type":"message","role":"assistant","model":"claude-opus-5","content":[{"type":"text","text":"Hello again! It's nice to see you. How can I assist you today? Is there anything specific you'd like to chat about or any questions you have?"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":11,"output_tokens":36}}}} {"custom_id":"my-first-request","result":{"type":"succeeded","message":{"id":"msg_01FqfsLoHwgeFbguDgpz48m7","type":"message","role":"assistant","model":"claude-opus-5","content":[{"type":"text","text":"Hello! How can I assist you today? Feel free to ask me any questions or let me know if there's anything you'd like to chat about."}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":10,"output_tokens":34}}}} ``` If your result has an error, its `result.error` will be set to the standard [error shape](https://platform.claude.com/docs/en/api/errors#error-shapes). **Batch results may not match input order** Batch results can be returned in any order, and may not match the ordering of requests when the batch was created. In the preceding example, the result for the second batch request is returned before the first. To correctly match results with their corresponding requests, always use the `custom_id` field. ### Canceling a Message Batch You can cancel a Message Batch that is currently processing using the [cancel endpoint](https://platform.claude.com/docs/en/api/canceling-message-batches). Immediately after cancellation, a batch's `processing_status` will be `canceling`. You can use the same polling technique described earlier to wait until cancellation is finalized. Canceled batches end up with a status of `ended` and may contain partial results for requests that were processed before cancellation. ```bash cURL #!/bin/sh # ... curl --request POST https://api.anthropic.com/v1/messages/batches/$MESSAGE_BATCH_ID/cancel \ --header "x-api-key: $ANTHROPIC_API_KEY" \ --header "anthropic-version: 2023-06-01" ``` ```bash CLI #!/bin/bash # ... ant messages:batches cancel --message-batch-id "$MESSAGE_BATCH_ID" ``` ```python Python client = anthropic.Anthropic() MESSAGE_BATCH_ID = "msgbatch_01HkcTjaV5uDC8jWR4ZsDV8d" message_batch = client.messages.batches.cancel( MESSAGE_BATCH_ID, ) print(message_batch) ``` ```typescript TypeScript const client = new Anthropic(); const messageBatch = await client.messages.batches.cancel(MESSAGE_BATCH_ID); console.log(messageBatch); ``` ```csharp C# AnthropicClient client = new(); string messageBatchId = Environment.GetEnvironmentVariable("MESSAGE_BATCH_ID"); var messageBatch = await client.Messages.Batches.Cancel(messageBatchId); Console.WriteLine(messageBatch); ``` ```go Go client := anthropic.NewClient() messageBatchID := os.Getenv("MESSAGE_BATCH_ID") messageBatch, err := client.Messages.Batches.Cancel(context.TODO(), messageBatchID) if err != nil { log.Fatal(err) } fmt.Println(messageBatch) ``` ```java Java import com.anthropic.models.messages.batches.*; // ... AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageBatch messageBatch = client .messages() .batches() .cancel("msgbatch_01HkcTjaV5uDC8jWR4ZsDV8d"); System.out.println(messageBatch); ``` ```php PHP $client = new Client(); $messageBatch = $client->messages->batches->cancel( messageBatchID: 'msgbatch_example_id', ); echo $messageBatch; ``` ```ruby Ruby client = Anthropic::Client.new message_batch_id = ENV.fetch("MESSAGE_BATCH_ID") message_batch = client.messages.batches.cancel(message_batch_id) puts message_batch ``` The response shows the batch in a `canceling` state: ```json Output { "id": "msgbatch_013Zva2CMHLNnXjNJJKqJ2EF", "type": "message_batch", "processing_status": "canceling", "request_counts": { "processing": 2, "succeeded": 0, "errored": 0, "canceled": 0, "expired": 0 }, "ended_at": null, "created_at": "2024-09-24T18:37:24.100435Z", "expires_at": "2024-09-25T18:37:24.100435Z", "cancel_initiated_at": "2024-09-24T18:39:03.114875Z", "results_url": null } ``` ### Using prompt caching with Message Batches The Message Batches API supports prompt caching, allowing you to potentially reduce costs and processing time for batch requests. The pricing discounts from prompt caching and Message Batches can stack, providing even greater cost savings when both features are used together. However, because batch requests are processed asynchronously and concurrently, cache hits are provided on a best-effort basis. Users typically experience cache hit rates ranging from 30% to 98%, depending on their traffic patterns. To maximize the likelihood of cache hits in your batch requests: 1. Include identical `cache_control` blocks in every Message request within your batch. 2. Maintain a steady stream of requests to prevent cache entries from expiring after their 5-minute lifetime. 3. Structure your requests to share as much cached content as possible. Example of implementing prompt caching in a batch: ```bash cURL curl https://api.anthropic.com/v1/messages/batches \ --header "x-api-key: $ANTHROPIC_API_KEY" \ --header "anthropic-version: 2023-06-01" \ --header "content-type: application/json" \ --data \ '{ "requests": [ { "custom_id": "my-first-request", "params": { "model": "claude-opus-5", "max_tokens": 1024, "system": [ { "type": "text", "text": "You are an AI assistant tasked with analyzing literary works. Your goal is to provide insightful commentary on themes, characters, and writing style.\n" }, { "type": "text", "text": "", "cache_control": {"type": "ephemeral"} } ], "messages": [ {"role": "user", "content": "Analyze the major themes in Pride and Prejudice."} ] } }, { "custom_id": "my-second-request", "params": { "model": "claude-opus-5", "max_tokens": 1024, "system": [ { "type": "text", "text": "You are an AI assistant tasked with analyzing literary works. Your goal is to provide insightful commentary on themes, characters, and writing style.\n" }, { "type": "text", "text": "", "cache_control": {"type": "ephemeral"} } ], "messages": [ {"role": "user", "content": "Write a summary of Pride and Prejudice."} ] } } ] }' ``` ```bash CLI ant messages:batches create <<'YAML' requests: - custom_id: my-first-request params: model: claude-opus-5 max_tokens: 1024 system: - type: text text: > You are an AI assistant tasked with analyzing literary works. Your goal is to provide insightful commentary on themes, characters, and writing style. - type: text text: "" cache_control: type: ephemeral messages: - role: user content: Analyze the major themes in Pride and Prejudice. - custom_id: my-second-request params: model: claude-opus-5 max_tokens: 1024 system: - type: text text: > You are an AI assistant tasked with analyzing literary works. Your goal is to provide insightful commentary on themes, characters, and writing style. - type: text text: "" cache_control: type: ephemeral messages: - role: user content: Write a summary of Pride and Prejudice. YAML ``` ```python Python from anthropic.types.message_create_params import MessageCreateParamsNonStreaming from anthropic.types.messages.batch_create_params import Request client = anthropic.Anthropic() message_batch = client.messages.batches.create( requests=[ Request( custom_id="my-first-request", params=MessageCreateParamsNonStreaming( model="claude-opus-5", max_tokens=1024, system=[ { "type": "text", "text": "You are an AI assistant tasked with analyzing literary works. Your goal is to provide insightful commentary on themes, characters, and writing style.\n", }, { "type": "text", "text": "", "cache_control": {"type": "ephemeral"}, }, ], messages=[ { "role": "user", "content": "Analyze the major themes in Pride and Prejudice.", } ], ), ), Request( custom_id="my-second-request", params=MessageCreateParamsNonStreaming( model="claude-opus-5", max_tokens=1024, system=[ { "type": "text", "text": "You are an AI assistant tasked with analyzing literary works. Your goal is to provide insightful commentary on themes, characters, and writing style.\n", }, { "type": "text", "text": "", "cache_control": {"type": "ephemeral"}, }, ], messages=[ { "role": "user", "content": "Write a summary of Pride and Prejudice.", } ], ), ), ] ) ``` ```typescript TypeScript const client = new Anthropic(); const messageBatch = await client.messages.batches.create({ requests: [ { custom_id: "my-first-request", params: { model: "claude-opus-5", max_tokens: 1024, system: [ { type: "text", text: "You are an AI assistant tasked with analyzing literary works. Your goal is to provide insightful commentary on themes, characters, and writing style.\n" }, { type: "text", text: "", cache_control: { type: "ephemeral" } } ], messages: [ { role: "user", content: "Analyze the major themes in Pride and Prejudice." } ] } }, { custom_id: "my-second-request", params: { model: "claude-opus-5", max_tokens: 1024, system: [ { type: "text", text: "You are an AI assistant tasked with analyzing literary works. Your goal is to provide insightful commentary on themes, characters, and writing style.\n" }, { type: "text", text: "", cache_control: { type: "ephemeral" } } ], messages: [{ role: "user", content: "Write a summary of Pride and Prejudice." }] } } ] }); ``` ```csharp C# using Anthropic; using Anthropic.Models.Messages; using Anthropic.Models.Messages.Batches; AnthropicClient client = new() { ApiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY") }; var messageBatch = await client.Messages.Batches.Create(new BatchCreateParams { Requests = [ new() { CustomID = "my-first-request", Params = new() { Model = Model.ClaudeOpus5, MaxTokens = 1024, System = new List { new() { Text = "You are an AI assistant tasked with analyzing literary works. Your goal is to provide insightful commentary on themes, characters, and writing style.\n" }, new() { Text = "", CacheControl = new() } }, Messages = [ new() { Role = Role.User, Content = "Analyze the major themes in Pride and Prejudice." } ] } }, new() { CustomID = "my-second-request", Params = new() { Model = Model.ClaudeOpus5, MaxTokens = 1024, System = new List { new() { Text = "You are an AI assistant tasked with analyzing literary works. Your goal is to provide insightful commentary on themes, characters, and writing style.\n" }, new() { Text = "", CacheControl = new() } }, Messages = [ new() { Role = Role.User, Content = "Write a summary of Pride and Prejudice." } ] } } ] }); ``` ```go Go client := anthropic.NewClient() messageBatch, err := client.Messages.Batches.New(context.TODO(), anthropic.MessageBatchNewParams{ Requests: []anthropic.MessageBatchNewParamsRequest{ { CustomID: "my-first-request", Params: anthropic.MessageBatchNewParamsRequestParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, System: []anthropic.TextBlockParam{ { Text: "You are an AI assistant tasked with analyzing literary works. Your goal is to provide insightful commentary on themes, characters, and writing style.\n", }, { Text: "", CacheControl: anthropic.NewCacheControlEphemeralParam(), }, }, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Analyze the major themes in Pride and Prejudice.")), }, }, }, { CustomID: "my-second-request", Params: anthropic.MessageBatchNewParamsRequestParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, System: []anthropic.TextBlockParam{ { Text: "You are an AI assistant tasked with analyzing literary works. Your goal is to provide insightful commentary on themes, characters, and writing style.\n", }, { Text: "", CacheControl: anthropic.NewCacheControlEphemeralParam(), }, }, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Write a summary of Pride and Prejudice.")), }, }, }, }, }) if err != nil { log.Fatal(err) } fmt.Println(messageBatch) ``` ```java Java import com.anthropic.models.messages.CacheControlEphemeral; // ... import com.anthropic.models.messages.batches.*; // ... AnthropicClient client = AnthropicOkHttpClient.fromEnv(); BatchCreateParams createParams = BatchCreateParams.builder() .addRequest( BatchCreateParams.Request.builder() .customId("my-first-request") .params( BatchCreateParams.Request.Params.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .systemOfTextBlockParams( List.of( TextBlockParam.builder() .text( "You are an AI assistant tasked with analyzing literary works. Your goal is to provide insightful commentary on themes, characters, and writing style.\n" ) .build(), TextBlockParam.builder() .text("") .cacheControl(CacheControlEphemeral.builder().build()) .build() ) ) .addUserMessage("Analyze the major themes in Pride and Prejudice.") .build() ) .build() ) .addRequest( BatchCreateParams.Request.builder() .customId("my-second-request") .params( BatchCreateParams.Request.Params.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .systemOfTextBlockParams( List.of( TextBlockParam.builder() .text( "You are an AI assistant tasked with analyzing literary works. Your goal is to provide insightful commentary on themes, characters, and writing style.\n" ) .build(), TextBlockParam.builder() .text("") .cacheControl(CacheControlEphemeral.builder().build()) .build() ) ) .addUserMessage("Write a summary of Pride and Prejudice.") .build() ) .build() ) .build(); MessageBatch messageBatch = client.messages().batches().create(createParams); ``` ```php PHP $client = new Client(); $messageBatch = $client->messages->batches->create( requests: [ [ 'custom_id' => 'my-first-request', 'params' => [ 'model' => 'claude-opus-5', 'max_tokens' => 1024, 'system' => [ [ 'type' => 'text', 'text' => 'You are an AI assistant tasked with analyzing literary works. Your goal is to provide insightful commentary on themes, characters, and writing style.\n' ], [ 'type' => 'text', 'text' => '', 'cache_control' => ['type' => 'ephemeral'] ] ], 'messages' => [ ['role' => 'user', 'content' => 'Analyze the major themes in Pride and Prejudice.'] ] ] ], [ 'custom_id' => 'my-second-request', 'params' => [ 'model' => 'claude-opus-5', 'max_tokens' => 1024, 'system' => [ [ 'type' => 'text', 'text' => 'You are an AI assistant tasked with analyzing literary works. Your goal is to provide insightful commentary on themes, characters, and writing style.\n' ], [ 'type' => 'text', 'text' => '', 'cache_control' => ['type' => 'ephemeral'] ] ], 'messages' => [ ['role' => 'user', 'content' => 'Write a summary of Pride and Prejudice.'] ] ] ] ], ); ``` ```ruby Ruby client = Anthropic::Client.new message_batch = client.messages.batches.create( requests: [ { custom_id: "my-first-request", params: { model: "claude-opus-5", max_tokens: 1024, system: [ { type: "text", text: "You are an AI assistant tasked with analyzing literary works. Your goal is to provide insightful commentary on themes, characters, and writing style.\n" }, { type: "text", text: "", cache_control: { type: "ephemeral" } } ], messages: [ { role: "user", content: "Analyze the major themes in Pride and Prejudice." } ] } }, { custom_id: "my-second-request", params: { model: "claude-opus-5", max_tokens: 1024, system: [ { type: "text", text: "You are an AI assistant tasked with analyzing literary works. Your goal is to provide insightful commentary on themes, characters, and writing style.\n" }, { type: "text", text: "", cache_control: { type: "ephemeral" } } ], messages: [ { role: "user", content: "Write a summary of Pride and Prejudice." } ] } } ] ) ``` In this example, both requests in the batch include identical system messages and the full text of Pride and Prejudice marked with `cache_control` to increase the likelihood of cache hits. ### Server tools and the agentic loop All [server tools](https://platform.claude.com/docs/en/agents-and-tools/tool-use/server-tools) (web search, web fetch, code execution, MCP connectors, advisor, and tool search) work in batch requests. The batch worker runs the same server-side agentic loop as the synchronous Messages API. Because there is no open connection to maintain, the batch loop runs **more iterations per turn** than a synchronous request before it returns `stop_reason: "pause_turn"`. If a batch result comes back with `pause_turn`, the turn did not finish; you can continue it by submitting the paused assistant content in a follow-up request (batch or synchronous) exactly as shown in the [pause\_turn continuation pattern](https://platform.claude.com/docs/en/agents-and-tools/tool-use/server-tools#the-server-side-loop-and-pause-turn). The batch worker additionally throttles `web_search` per organization so that highly concurrent batch processing does not exhaust your organization's web-search rate limit. The batch retries throttled requests automatically; you don't need to handle this yourself, but very large web-search batches might take longer to complete. ### Extended output (beta) The `output-300k-2026-03-24` beta header raises the `max_tokens` cap to 300,000 for batch requests using Claude Opus 5, Claude Opus 4.8, Claude Opus 4.7, Claude Opus 4.6, Claude Sonnet 5, or Claude Sonnet 4.6. Include the header to generate outputs far longer than the standard 128k `max_tokens` limit in a single turn. Extended output is available on the Message Batches API only, not the synchronous Messages API. It is supported on the Claude API and Claude Platform on AWS, and is not currently available on Amazon Bedrock, Google Cloud, or Microsoft Foundry. Use extended output for long-form generation such as book-length drafts and technical documentation, exhaustive structured data extraction, large code-generation scaffolds, and long reasoning chains. A single 300k-token generation can take over an hour to complete, so plan your batch submissions with the 24-hour processing window in mind. Standard batch pricing (50% of standard API prices) applies. ```bash cURL curl https://api.anthropic.com/v1/messages/batches \ --header "x-api-key: $ANTHROPIC_API_KEY" \ --header "anthropic-version: 2023-06-01" \ --header "anthropic-beta: output-300k-2026-03-24" \ --header "content-type: application/json" \ --data \ '{ "requests": [ { "custom_id": "long-form-request", "params": { "model": "claude-opus-5", "max_tokens": 300000, "messages": [ {"role": "user", "content": "Write a comprehensive technical guide to building distributed systems, covering architecture patterns, consistency models, fault tolerance, and operational best practices."} ] } } ] }' ``` ```bash CLI ant beta:messages:batches create --beta output-300k-2026-03-24 <<'YAML' requests: - custom_id: long-form-request params: model: claude-opus-5 max_tokens: 300000 messages: - role: user content: >- Write a comprehensive technical guide to building distributed systems, covering architecture patterns, consistency models, fault tolerance, and operational best practices. YAML ``` ```python Python from anthropic.types.beta.message_create_params import MessageCreateParamsNonStreaming from anthropic.types.beta.messages.batch_create_params import Request client = anthropic.Anthropic() message_batch = client.beta.messages.batches.create( betas=["output-300k-2026-03-24"], requests=[ Request( custom_id="long-form-request", params=MessageCreateParamsNonStreaming( model="claude-opus-5", max_tokens=300_000, messages=[ { "role": "user", "content": "Write a comprehensive technical guide to building distributed systems, covering architecture patterns, consistency models, fault tolerance, and operational best practices.", } ], ), ), ], ) print(message_batch) ``` ```typescript TypeScript const client = new Anthropic(); const messageBatch = await client.beta.messages.batches.create({ betas: ["output-300k-2026-03-24"], requests: [ { custom_id: "long-form-request", params: { model: "claude-opus-5", max_tokens: 300000, messages: [ { role: "user", content: "Write a comprehensive technical guide to building distributed systems, covering architecture patterns, consistency models, fault tolerance, and operational best practices." } ] } } ] }); console.log(messageBatch); ``` ```csharp C# using Anthropic; using Anthropic.Models.Beta.Messages; using Anthropic.Models.Beta.Messages.Batches; using Model = Anthropic.Models.Messages.Model; AnthropicClient client = new(); var batch = await client.Beta.Messages.Batches.Create(new BatchCreateParams { Betas = ["output-300k-2026-03-24"], Requests = [ new() { CustomID = "long-form-request", Params = new() { Model = Model.ClaudeOpus5, MaxTokens = 300_000, Messages = [ new() { Role = Role.User, Content = "Write a comprehensive technical guide to building distributed systems, covering architecture patterns, consistency models, fault tolerance, and operational best practices." } ] } } ] }); Console.WriteLine(batch); ``` ```go Go client := anthropic.NewClient() batch, err := client.Beta.Messages.Batches.New(context.Background(), anthropic.BetaMessageBatchNewParams{ Betas: []anthropic.AnthropicBeta{"output-300k-2026-03-24"}, Requests: []anthropic.BetaMessageBatchNewParamsRequest{ { CustomID: "long-form-request", Params: anthropic.BetaMessageBatchNewParamsRequestParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 300_000, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage( anthropic.NewBetaTextBlock("Write a comprehensive technical guide to building distributed systems, covering architecture patterns, consistency models, fault tolerance, and operational best practices."), ), }, }, }, }, }) if err != nil { panic(err) } fmt.Println(batch.ID) ``` ```java Java import com.anthropic.models.beta.messages.batches.*; void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); BatchCreateParams params = BatchCreateParams.builder() .addBeta("output-300k-2026-03-24") .addRequest( BatchCreateParams.Request.builder() .customId("long-form-request") .params( BatchCreateParams.Request.Params.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(300_000L) .addUserMessage("Write a comprehensive technical guide to building distributed systems, covering architecture patterns, consistency models, fault tolerance, and operational best practices.") .build() ) .build() ) .build(); BetaMessageBatch messageBatch = client.beta().messages().batches().create(params); IO.println(messageBatch); } ``` ```php PHP $client = new Client(); $batch = $client->beta->messages->batches->create( betas: ['output-300k-2026-03-24'], requests: [ [ 'custom_id' => 'long-form-request', 'params' => [ 'model' => 'claude-opus-5', 'max_tokens' => 300_000, 'messages' => [ ['role' => 'user', 'content' => 'Write a comprehensive technical guide to building distributed systems, covering architecture patterns, consistency models, fault tolerance, and operational best practices.'] ] ] ] ], ); echo $batch->id; ``` ```ruby Ruby client = Anthropic::Client.new batch = client.beta.messages.batches.create( betas: ["output-300k-2026-03-24"], requests: [ { custom_id: "long-form-request", params: { model: "claude-opus-5", max_tokens: 300_000, messages: [ { role: "user", content: "Write a comprehensive technical guide to building distributed systems, covering architecture patterns, consistency models, fault tolerance, and operational best practices." } ] } } ] ) puts batch ``` ### Best practices for effective batching To get the most out of the Batches API: * Monitor batch processing status regularly and implement appropriate retry logic for failed requests. * Use meaningful `custom_id` values to easily match results with requests, since order is not guaranteed. * Consider breaking very large datasets into multiple batches for better manageability. * Dry run a single request shape with the Messages API to avoid validation errors. ### Troubleshooting common issues If experiencing unexpected behavior: * Verify that the total batch request size doesn't exceed 256 MB. If the request size is too large, you may get a 413 `request_too_large` error. * Check that you're using [supported models](https://platform.claude.com/docs/en/build-with-claude/batch-processing#supported-models) for all requests in the batch. * Ensure each request in the batch has a unique `custom_id`. * Ensure that it has been less than 29 days since batch `created_at` (not processing `ended_at`) time. If over 29 days have passed, results will no longer be viewable. * Confirm that the batch has not been canceled. Note that the failure of one request in a batch does not affect the processing of other requests. ## Batch storage and privacy * **Workspace isolation**: Batches are isolated within the Workspace they are created in. They can only be accessed by API keys associated with that Workspace, or users with permission to view Workspace batches in the Console. * **Result availability**: Batch results are available for 29 days after the batch is created, allowing ample time for retrieval and processing. ## Data retention Batch processing stores request and response data for up to 29 days after batch creation. You can delete a message batch at any time after processing using the `DELETE /v1/messages/batches/{batch_id}` endpoint. To delete an in-progress batch, cancel it first. Asynchronous processing requires server-side storage of both inputs and outputs until batch completion and result retrieval. For ZDR eligibility across all features, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention). ## FAQ Batches may take up to 24 hours for processing, but many finish sooner. Actual processing time depends on the size of the batch, current demand, and your request volume. It is possible for a batch to expire and not complete within 24 hours. See [Supported models](https://platform.claude.com/docs/en/build-with-claude/batch-processing#supported-models) for the list of supported models. Yes, the Message Batches API supports nearly all features available in the Messages API, including most beta features. A small number of parameters (`stream`, `speed`, `store`, `previous_thread_event_id`, `cache_hint`, `context_hint`, `max_tokens: 0`, and `research_preview_2026_02`) are not supported. See [What can be batched](https://platform.claude.com/docs/en/build-with-claude/batch-processing#what-can-be-batched) for the full list. The Message Batches API offers a 50% discount on all usage compared to standard API prices. This applies to input tokens, output tokens, and any special tokens. For more on pricing, visit [Pricing](https://claude.com/pricing#anthropic-api). No, once a batch has been submitted, it cannot be modified. If you need to make changes, you should cancel the current batch and submit a new one. Note that cancellation may not take immediate effect. The Message Batches API has HTTP requests-based rate limits in addition to limits on the number of requests in need of processing. See [Message Batches API rate limits](https://platform.claude.com/docs/en/api/rate-limits#message-batches-api). Usage of the Batches API does not affect rate limits in the Messages API. When you retrieve the results, each request has a `result` field indicating whether it `succeeded`, `errored`, was `canceled`, or `expired`. For `errored` results, additional error information is provided. View the error response object in the [API reference](https://platform.claude.com/docs/en/api/messages/batches/create). The Message Batches API is designed with strong privacy and data separation measures: 1. Batches and their results are isolated within the Workspace in which they were created. This means they can only be accessed by API keys from that same Workspace. 2. Each request within a batch is processed independently, with no data leakage between requests. 3. Results are only available for a limited time (29 days), and follow Anthropic's [data retention policy](https://support.claude.com/en/articles/7996866-how-long-do-you-store-personal-data). 4. Downloading batch results in the Console can be disabled on the organization-level or on a per-workspace basis. Yes, it is possible to use prompt caching with Message Batches API. However, because asynchronous batch requests can be processed concurrently and in any order, cache hits are provided on a best-effort basis. ## Next steps Enable natural citations for RAG applications by providing search results with source attribution. Reduce cost and latency by caching prompt prefixes shared across requests in a batch. --- title: Citations url: https://platform.claude.com/docs/en/build-with-claude/citations description: Ground Claude's responses in your source documents. Citations return the exact passages that support each claim, so you can verify answers and surface sources to your users. --- ## Compatibility - [ZDR](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention): eligible (excludes [Covered Models](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#model-specific-data-retention-requirements)) - Platforms: Claude API, Claude Platform on AWS, Amazon Bedrock, Google Cloud, Microsoft Foundry Claude can provide detailed citations when answering questions about documents, helping you track and verify the sources behind each response. All [active models](https://platform.claude.com/docs/en/about-claude/models/overview) support citations. Share your feedback and suggestions about the citations feature using the [citations feedback form](https://forms.gle/9n9hSrKnKe3rpowH9). The following example shows how to enable citations on a plain text document with the Messages API: ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "messages": [ { "role": "user", "content": [ { "type": "document", "source": { "type": "text", "media_type": "text/plain", "data": "The grass is green. The sky is blue." }, "title": "My Document", "context": "This is a trustworthy document.", "citations": {"enabled": true} }, { "type": "text", "text": "What color is the grass and sky?" } ] } ] }' ``` ```bash CLI ant messages create <<'YAML' model: claude-opus-5 max_tokens: 1024 messages: - role: user content: - type: document source: type: text media_type: text/plain data: The grass is green. The sky is blue. title: My Document context: This is a trustworthy document. citations: enabled: true - type: text text: What color is the grass and sky? YAML ``` ```python Python client = anthropic.Anthropic() response = client.messages.create( model="claude-opus-5", max_tokens=1024, messages=[ { "role": "user", "content": [ { "type": "document", "source": { "type": "text", "media_type": "text/plain", "data": "The grass is green. The sky is blue.", }, "title": "My Document", "context": "This is a trustworthy document.", "citations": {"enabled": True}, }, {"type": "text", "text": "What color is the grass and sky?"}, ], } ], ) print(response) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: [ { type: "document", source: { type: "text", media_type: "text/plain", data: "The grass is green. The sky is blue." }, title: "My Document", context: "This is a trustworthy document.", citations: { enabled: true } }, { type: "text", text: "What color is the grass and sky?" } ] } ] }); console.log(response); ``` ```csharp C# var client = new AnthropicClient(); var response = await client.Messages.Create( new() { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [ new() { Role = Role.User, Content = new MessageParamContent(new List { new ContentBlockParam(new DocumentBlockParam( new DocumentBlockParamSource(new PlainTextSource() { Data = "The grass is green. The sky is blue.", }) ) { Title = "My Document", Context = "This is a trustworthy document.", Citations = new CitationsConfigParam { Enabled = true }, }), new ContentBlockParam(new TextBlockParam("What color is the grass and sky?")), }), }, ], } ); Console.WriteLine(response); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage( anthropic.ContentBlockParamUnion{ OfDocument: &anthropic.DocumentBlockParam{ Source: anthropic.DocumentBlockParamSourceUnion{ OfText: &anthropic.PlainTextSourceParam{ Data: "The grass is green. The sky is blue.", }, }, Title: anthropic.String("My Document"), Context: anthropic.String("This is a trustworthy document."), Citations: anthropic.CitationsConfigParam{Enabled: anthropic.Bool(true)}, }, }, anthropic.NewTextBlock("What color is the grass and sky?"), ), }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); PlainTextSource source = PlainTextSource.builder() .data("The grass is green. The sky is blue.") .build(); DocumentBlockParam documentParam = DocumentBlockParam.builder() .source(source) .title("My Document") .context("This is a trustworthy document.") .citations(CitationsConfigParam.builder().enabled(true).build()) .build(); TextBlockParam textBlockParam = TextBlockParam.builder() .text("What color is the grass and sky?") .build(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .addUserMessageOfBlockParams( List.of( ContentBlockParam.ofDocument(documentParam), ContentBlockParam.ofText(textBlockParam) ) ) .build(); Message message = client.messages().create(params); System.out.println(message); ``` ```php PHP $client = new Client(); $response = $client->messages->create( maxTokens: 1024, messages: [ [ 'role' => 'user', 'content' => [ [ 'type' => 'document', 'source' => [ 'type' => 'text', 'media_type' => 'text/plain', 'data' => 'The grass is green. The sky is blue.', ], 'title' => 'My Document', 'context' => 'This is a trustworthy document.', 'citations' => ['enabled' => true], ], [ 'type' => 'text', 'text' => 'What color is the grass and sky?', ], ], ], ], model: 'claude-opus-5', ); echo json_encode($response, JSON_PRETTY_PRINT); ``` ```ruby Ruby client = Anthropic::Client.new response = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: [ { type: "document", source: { type: "text", media_type: "text/plain", data: "The grass is green. The sky is blue." }, title: "My Document", context: "This is a trustworthy document.", citations: { enabled: true } }, { type: "text", text: "What color is the grass and sky?" } ] } ] ) puts response ``` **Comparison with prompt-based approaches** Compared to prompting Claude to cite sources, the citations feature offers the following advantages: * **Cost savings:** If your prompt-based approach asks Claude to output direct quotes, you may see cost savings because `cited_text` does not count toward your output tokens. * **Better citation reliability:** Because the API parses citations into the response formats described in the following sections and extracts `cited_text` directly, citations are guaranteed to contain valid pointers to the provided documents. * **Improved citation quality:** In Anthropic's evaluations, the citations feature is significantly more likely to cite the most relevant quotes from documents than purely prompt-based approaches. *** ## How citations work Integrate citations with Claude in these steps: * Include documents in any of the supported formats: [PDFs](https://platform.claude.com/docs/en/build-with-claude/citations#pdf-documents), [plain text](https://platform.claude.com/docs/en/build-with-claude/citations#plain-text-documents), or [custom content](https://platform.claude.com/docs/en/build-with-claude/citations#custom-content-documents) documents. * Set `citations.enabled=true` on each of your documents. Currently, citations must be enabled on all or none of the documents within a request. * Only text citations are currently supported. Image citations are not yet possible. * Document contents are "chunked" to define the minimum granularity of possible citations. For example, sentence chunking lets Claude cite a single sentence or chain together multiple consecutive sentences to cite a paragraph or longer passage. * **For PDFs:** Text is extracted as described in [PDF support](https://platform.claude.com/docs/en/build-with-claude/pdf-support) and content is chunked into sentences. Citing images from PDFs is not currently supported. * **For plain text documents:** Content is chunked into sentences that can be cited from. * **For custom content documents:** Your provided content blocks are used as-is and no further chunking is done. * Responses may now include multiple text blocks where each text block can contain a claim that Claude is making and a list of citations that support the claim. * Citations reference specific locations in source documents. The format of these citations is dependent on the type of document being cited from. * **For PDFs:** Citations include the page number range (1-indexed). * **For plain text documents:** Citations include the character index range (0-indexed). * **For custom content documents:** Citations include the content block index range (0-indexed) corresponding to the original content list provided. * Document indices are provided to indicate the reference source and are 0-indexed according to the list of all documents in your original request. **Automatic chunking vs custom content** By default, plain text and PDF documents are automatically chunked into sentences. If you need more control over citation granularity (for example, for bullet points or transcripts), use custom content documents instead. See [Document types](https://platform.claude.com/docs/en/build-with-claude/citations#document-types) for more details. For example, if you want Claude to be able to cite specific sentences from your RAG chunks, you should put each RAG chunk into a plain text document. Otherwise, if you do not want any further chunking to be done, or if you want to customize any additional chunking, you can put RAG chunks into custom content document(s). ### Citable versus non-citable content * Text found within a document's `source` content can be cited from. * `title` and `context` are optional fields that are passed to the model but not used toward cited content. * `title` is limited in length, so the `context` field is useful for storing document metadata as text or stringified JSON. ### Citation indices * Document indices are 0-indexed from the list of all document content blocks in the request (spanning across all messages). * Character indices are 0-indexed with exclusive end indices. * Page numbers are 1-indexed with exclusive end page numbers. * Content block indices are 0-indexed with exclusive end indices from the `content` list provided in the custom content document. ### Token costs * Enabling citations incurs a slight increase in input tokens because of system prompt additions and document chunking. * However, the citations feature is very efficient with output tokens. Internally, the model outputs citations in a standardized format that are then parsed into cited text and document location indices. The `cited_text` field is provided for convenience and does not count toward output tokens. * When passed back in subsequent conversation turns, `cited_text` is also not counted toward input tokens. ### Feature compatibility Citations work in conjunction with other API features including [prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching), [token counting](https://platform.claude.com/docs/en/build-with-claude/token-counting), and [batch processing](https://platform.claude.com/docs/en/build-with-claude/batch-processing). **Citations and structured outputs are incompatible** Citations cannot be used together with [structured outputs](https://platform.claude.com/docs/en/build-with-claude/structured-outputs). If you enable citations on any user-provided document (`document` blocks or `search_result` blocks) and also include the `output_config.format` parameter (or the deprecated `output_format` parameter), the API returns a 400 error. This is because citations require interleaving citation blocks with text output, which is incompatible with the strict JSON schema constraints of structured outputs. #### Using prompt caching with citations Citations and prompt caching can be used together effectively. The citation blocks generated in responses cannot be cached directly, but the source documents they reference can be cached. To optimize performance, apply `cache_control` to your top-level document content blocks. ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "messages": [ { "role": "user", "content": [ { "type": "document", "source": { "type": "text", "media_type": "text/plain", "data": "This is a very long document with thousands of words..." }, "citations": {"enabled": true}, "cache_control": {"type": "ephemeral"} }, { "type": "text", "text": "What does this document say about API features?" } ] } ] }' ``` ```bash CLI ant messages create \ --model claude-opus-5 \ --max-tokens 1024 <<'YAML' messages: - role: user content: - type: document source: type: text media_type: text/plain data: This is a very long document with thousands of words... citations: enabled: true cache_control: type: ephemeral - type: text text: What does this document say about API features? YAML ``` ```python Python client = anthropic.Anthropic() # Long document content (for example, technical documentation) long_document = ( "This is a very long document with thousands of words..." + " ... " * 1000 ) # Minimum cacheable length response = client.messages.create( model="claude-opus-5", max_tokens=1024, messages=[ { "role": "user", "content": [ { "type": "document", "source": { "type": "text", "media_type": "text/plain", "data": long_document, }, "citations": {"enabled": True}, "cache_control": { "type": "ephemeral" }, # Cache the document content }, { "type": "text", "text": "What does this document say about API features?", }, ], } ], ) print(response) ``` ```typescript TypeScript const client = new Anthropic(); // Long document content (for example, technical documentation) const longDocument = "This is a very long document with thousands of words..." + " ... ".repeat(1000); // Minimum cacheable length const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: [ { type: "document", source: { type: "text", media_type: "text/plain", data: longDocument }, citations: { enabled: true }, cache_control: { type: "ephemeral" } // Cache the document content }, { type: "text", text: "What does this document say about API features?" } ] } ] }); console.log(response); ``` ```csharp C# var client = new AnthropicClient(); // Long document content (for example, technical documentation) var longDocument = "This is a very long document with thousands of words..." + string.Concat(Enumerable.Repeat(" ... ", 1000)); // Minimum cacheable length var response = await client.Messages.Create( new() { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [ new() { Role = Role.User, Content = new MessageParamContent(new List { new ContentBlockParam(new DocumentBlockParam( new DocumentBlockParamSource(new PlainTextSource() { Data = longDocument }) ) { Citations = new CitationsConfigParam { Enabled = true }, CacheControl = new CacheControlEphemeral(), // Cache the document content }), new ContentBlockParam(new TextBlockParam("What does this document say about API features?")), }), }, ], } ); Console.WriteLine(response); ``` ```go Go client := anthropic.NewClient() // Long document content (for example, technical documentation) longDocument := "This is a very long document with thousands of words..." + strings.Repeat(" ... ", 1000) // Minimum cacheable length response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage( anthropic.ContentBlockParamUnion{ OfDocument: &anthropic.DocumentBlockParam{ Source: anthropic.DocumentBlockParamSourceUnion{ OfText: &anthropic.PlainTextSourceParam{Data: longDocument}, }, Citations: anthropic.CitationsConfigParam{Enabled: anthropic.Bool(true)}, CacheControl: anthropic.NewCacheControlEphemeralParam(), // Cache the document content }, }, anthropic.NewTextBlock("What does this document say about API features?"), ), }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); // Long document content (for example, technical documentation) String longDocument = "This is a very long document with thousands of words..." + " ... ".repeat(1000); // Minimum cacheable length DocumentBlockParam documentParam = DocumentBlockParam.builder() .source(PlainTextSource.builder().data(longDocument).build()) .citations(CitationsConfigParam.builder().enabled(true).build()) .cacheControl(CacheControlEphemeral.builder().build()) // Cache the document content .build(); TextBlockParam textBlockParam = TextBlockParam.builder() .text("What does this document say about API features?") .build(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .addUserMessageOfBlockParams( List.of( ContentBlockParam.ofDocument(documentParam), ContentBlockParam.ofText(textBlockParam) ) ) .build(); Message message = client.messages().create(params); System.out.println(message); ``` ```php PHP $client = new Client(); // Long document content (for example, technical documentation) $longDocument = 'This is a very long document with thousands of words...' . str_repeat(' ... ', 1000); // Minimum cacheable length $response = $client->messages->create( maxTokens: 1024, messages: [ [ 'role' => 'user', 'content' => [ [ 'type' => 'document', 'source' => [ 'type' => 'text', 'media_type' => 'text/plain', 'data' => $longDocument, ], 'citations' => ['enabled' => true], 'cache_control' => ['type' => 'ephemeral'], // Cache the document content ], [ 'type' => 'text', 'text' => 'What does this document say about API features?', ], ], ], ], model: 'claude-opus-5', ); echo json_encode($response, JSON_PRETTY_PRINT); ``` ```ruby Ruby client = Anthropic::Client.new # Long document content (for example, technical documentation) long_document = "This is a very long document with thousands of words..." + " ... " * 1000 # Minimum cacheable length response = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: [ { type: "document", source: { type: "text", media_type: "text/plain", data: long_document }, citations: { enabled: true }, cache_control: { type: "ephemeral" } # Cache the document content }, { type: "text", text: "What does this document say about API features?" } ] } ] ) puts response ``` In this example: * The document content is cached using `cache_control` on the document block. * Citations are enabled on the document. * Claude can generate responses with citations while benefiting from cached document content. * Subsequent requests using the same document benefit from the cached content. ## Document types ### Choosing a document type Three document types are supported for citations. Documents can be provided directly in the message (base64, text, or URL) or uploaded through the [Files API](https://platform.claude.com/docs/en/build-with-claude/files) and referenced by `file_id`: | Type | Best for | Chunking | Citation format | | -------------- | --------------------------------------------------------------- | ---------------------- | ----------------------------- | | Plain text | Simple text documents, prose | Sentence | Character indices (0-indexed) | | PDF | PDF files with text content | Sentence | Page numbers (1-indexed) | | Custom content | Lists, transcripts, special formatting, more granular citations | No additional chunking | Block indices (0-indexed) | For file types that the `document` block doesn't support (for example, .docx and .xlsx), convert the files to plain text and include the content directly in message content. Files that are already plain text, such as .csv and .md files, can also be uploaded with an explicit `text/plain` content type. See [Working with other file formats](https://platform.claude.com/docs/en/build-with-claude/files#working-with-other-file-formats). ### Plain text documents Plain text documents are automatically chunked into sentences. You can provide them inline or by reference with their `file_id`: The intro example at the top of this page shows a complete plain text request in every SDK. The document block uses a `text` source: ```json { "type": "document", "source": { "type": "text", "media_type": "text/plain", "data": "Plain text content..." }, "title": "Document Title", "context": "Context about the document that will not be cited from", "citations": { "enabled": true } } ``` Files API document sources are in beta. These examples use the beta client path; see [Files API](https://platform.claude.com/docs/en/build-with-claude/files) for upload details. ```bash cURL curl -X POST https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: files-api-2025-04-14" \ -H "content-type: application/json" \ -d @- < { new BetaRequestDocumentBlock { Source = new BetaFileDocumentSource { FileID = fileId }, Title = "Document Title", Context = "Context about the document that will not be cited from", Citations = new BetaCitationsConfigParam { Enabled = true }, }, new BetaTextBlockParam { Text = "Summarize this document." }, } } ] }); Console.WriteLine(citedResponse); ``` ```go Go citedMsg, err := client.Beta.Messages.New(context.Background(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Betas: []anthropic.AnthropicBeta{anthropic.AnthropicBetaFilesAPI2025_04_14}, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage( anthropic.BetaContentBlockParamUnion{ OfDocument: &anthropic.BetaRequestDocumentBlockParam{ Source: anthropic.BetaRequestDocumentBlockSourceUnionParam{ OfFile: &anthropic.BetaFileDocumentSourceParam{FileID: fileID}, }, Title: anthropic.String("Document Title"), Context: anthropic.String("Context about the document that will not be cited from"), Citations: anthropic.BetaCitationsConfigParam{Enabled: anthropic.Bool(true)}, }, }, anthropic.NewBetaTextBlock("Summarize this document."), ), }, }) if err != nil { log.Fatal(err) } fmt.Println(citedMsg) ``` ```java Java MessageCreateParams citedParams = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .addBeta("files-api-2025-04-14") .maxTokens(1024) .addUserMessageOfBetaContentBlockParams(List.of( BetaContentBlockParam.ofDocument(BetaRequestDocumentBlock.builder() .source(BetaFileDocumentSource.builder().fileId(fileId).build()) .title("Document Title") .context("Context about the document that will not be cited from") .citations(BetaCitationsConfigParam.builder().enabled(true).build()) .build()), BetaContentBlockParam.ofText(BetaTextBlockParam.builder() .text("Summarize this document.") .build()) )) .build(); BetaMessage citedMessage = client.beta().messages().create(citedParams); System.out.println(citedMessage); ``` ```php PHP $citedResponse = $client->beta->messages->create( maxTokens: 1024, messages: [ [ 'role' => 'user', 'content' => [ [ 'type' => 'document', 'source' => ['type' => 'file', 'file_id' => $fileId], 'title' => 'Document Title', 'context' => 'Context about the document that will not be cited from', 'citations' => ['enabled' => true], ], ['type' => 'text', 'text' => 'Summarize this document.'], ], ], ], model: 'claude-opus-5', betas: ['files-api-2025-04-14'], ); print_r($citedResponse); ``` ```ruby Ruby cited_response = client.beta.messages.create( model: "claude-opus-5", max_tokens: 1024, betas: ["files-api-2025-04-14"], messages: [ { role: "user", content: [ { type: "document", source: { type: "file", file_id: file_id }, title: "Document Title", context: "Context about the document that will not be cited from", citations: { enabled: true } }, { type: "text", text: "Summarize this document." } ] } ] ) puts cited_response ``` ```json { "type": "char_location", "cited_text": "The exact text being cited", // not counted toward output tokens "document_index": 0, "document_title": "Document Title", "start_char_index": 0, // 0-indexed "end_char_index": 50 // exclusive } ``` ### PDF documents PDF documents can be provided as base64-encoded data, a URL, or by `file_id`. PDF text is extracted and chunked into sentences. As image citations are not yet supported, PDFs that are scans of documents and do not contain extractable text are not citable. ```bash cURL PDF_BASE64=$(base64 /path/to/document.pdf | tr -d '\n') curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "messages": [ { "role": "user", "content": [ { "type": "document", "source": { "type": "base64", "media_type": "application/pdf", "data": "'"$PDF_BASE64"'" }, "title": "Document Title", "context": "Context about the document that will not be cited from", "citations": {"enabled": true} }, { "type": "text", "text": "Summarize this document." } ] } ] }' ``` ```bash CLI ant messages create <<'YAML' model: claude-opus-5 max_tokens: 1024 messages: - role: user content: - type: document source: type: base64 media_type: application/pdf data: "@/path/to/document.pdf" title: Document Title context: Context about the document that will not be cited from citations: enabled: true - type: text text: Summarize this document. YAML ``` ```python Python client = anthropic.Anthropic() pdf_base64 = base64.standard_b64encode( pathlib.Path("/path/to/document.pdf").read_bytes() ).decode() response = client.messages.create( model="claude-opus-5", max_tokens=1024, messages=[ { "role": "user", "content": [ { "type": "document", "source": { "type": "base64", "media_type": "application/pdf", "data": pdf_base64, }, "title": "Document Title", "context": "Context about the document that will not be cited from", "citations": {"enabled": True}, }, {"type": "text", "text": "Summarize this document."}, ], } ], ) print(response) ``` ```typescript TypeScript const client = new Anthropic(); const pdfBase64 = Buffer.from(await readFile("/path/to/document.pdf")).toString("base64"); const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: [ { type: "document", source: { type: "base64", media_type: "application/pdf", data: pdfBase64 }, title: "Document Title", context: "Context about the document that will not be cited from", citations: { enabled: true } }, { type: "text", text: "Summarize this document." } ] } ] }); console.log(response); ``` ```csharp C# var client = new AnthropicClient(); var pdfBase64 = Convert.ToBase64String(await File.ReadAllBytesAsync("/path/to/document.pdf")); var response = await client.Messages.Create( new() { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [ new() { Role = Role.User, Content = new MessageParamContent(new List { new ContentBlockParam(new DocumentBlockParam( new DocumentBlockParamSource(new Base64PdfSource() { Data = pdfBase64 }) ) { Title = "Document Title", Context = "Context about the document that will not be cited from", Citations = new CitationsConfigParam { Enabled = true }, }), new ContentBlockParam(new TextBlockParam("Summarize this document.")), }), }, ], } ); Console.WriteLine(response); ``` ```go Go client := anthropic.NewClient() pdfBytes, err := os.ReadFile("/path/to/document.pdf") if err != nil { log.Fatal(err) } pdfBase64 := base64.StdEncoding.EncodeToString(pdfBytes) response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage( anthropic.ContentBlockParamUnion{ OfDocument: &anthropic.DocumentBlockParam{ Source: anthropic.DocumentBlockParamSourceUnion{ OfBase64: &anthropic.Base64PDFSourceParam{ Data: pdfBase64, }, }, Title: anthropic.String("Document Title"), Context: anthropic.String("Context about the document that will not be cited from"), Citations: anthropic.CitationsConfigParam{Enabled: anthropic.Bool(true)}, }, }, anthropic.NewTextBlock("Summarize this document."), ), }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); byte[] pdfBytes = Files.readAllBytes(Path.of("/path/to/document.pdf")); String pdfBase64 = Base64.getEncoder().encodeToString(pdfBytes); DocumentBlockParam documentParam = DocumentBlockParam.builder() .source(Base64PdfSource.builder().data(pdfBase64).build()) .title("Document Title") .context("Context about the document that will not be cited from") .citations(CitationsConfigParam.builder().enabled(true).build()) .build(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .addUserMessageOfBlockParams( List.of( ContentBlockParam.ofDocument(documentParam), ContentBlockParam.ofText(TextBlockParam.builder().text("Summarize this document.").build()) ) ) .build(); Message message = client.messages().create(params); System.out.println(message); ``` ```php PHP $client = new Client(); $pdfBase64 = base64_encode(file_get_contents('/path/to/document.pdf')); $response = $client->messages->create( maxTokens: 1024, messages: [ [ 'role' => 'user', 'content' => [ [ 'type' => 'document', 'source' => [ 'type' => 'base64', 'media_type' => 'application/pdf', 'data' => $pdfBase64, ], 'title' => 'Document Title', 'context' => 'Context about the document that will not be cited from', 'citations' => ['enabled' => true], ], [ 'type' => 'text', 'text' => 'Summarize this document.', ], ], ], ], model: 'claude-opus-5', ); echo json_encode($response, JSON_PRETTY_PRINT); ``` ```ruby Ruby client = Anthropic::Client.new pdf_base64 = Base64.strict_encode64(File.binread("/path/to/document.pdf")) response = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: [ { type: "document", source: { type: "base64", media_type: "application/pdf", data: pdf_base64 }, title: "Document Title", context: "Context about the document that will not be cited from", citations: { enabled: true } }, { type: "text", text: "Summarize this document." } ] } ] ) puts response ``` ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "messages": [ { "role": "user", "content": [ { "type": "document", "source": { "type": "url", "url": "https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf" }, "title": "Document Title", "context": "Context about the document that will not be cited from", "citations": {"enabled": true} }, { "type": "text", "text": "Summarize this document." } ] } ] }' ``` ```bash CLI ant messages create <<'YAML' model: claude-opus-5 max_tokens: 1024 messages: - role: user content: - type: document source: type: url url: https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf title: Document Title context: Context about the document that will not be cited from citations: enabled: true - type: text text: Summarize this document. YAML ``` ```python Python client = anthropic.Anthropic() response = client.messages.create( model="claude-opus-5", max_tokens=1024, messages=[ { "role": "user", "content": [ { "type": "document", "source": { "type": "url", "url": "https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf", }, "title": "Document Title", "context": "Context about the document that will not be cited from", "citations": {"enabled": True}, }, {"type": "text", "text": "Summarize this document."}, ], } ], ) print(response) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: [ { type: "document", source: { type: "url", url: "https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf" }, title: "Document Title", context: "Context about the document that will not be cited from", citations: { enabled: true } }, { type: "text", text: "Summarize this document." } ] } ] }); console.log(response); ``` ```csharp C# var client = new AnthropicClient(); var response = await client.Messages.Create( new() { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [ new() { Role = Role.User, Content = new MessageParamContent(new List { new ContentBlockParam(new DocumentBlockParam( new DocumentBlockParamSource(new UrlPdfSource() { Url = "https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf", }) ) { Title = "Document Title", Context = "Context about the document that will not be cited from", Citations = new CitationsConfigParam { Enabled = true }, }), new ContentBlockParam(new TextBlockParam("Summarize this document.")), }), }, ], } ); Console.WriteLine(response); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage( anthropic.ContentBlockParamUnion{ OfDocument: &anthropic.DocumentBlockParam{ Source: anthropic.DocumentBlockParamSourceUnion{ OfURL: &anthropic.URLPDFSourceParam{ URL: "https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf", }, }, Title: anthropic.String("Document Title"), Context: anthropic.String("Context about the document that will not be cited from"), Citations: anthropic.CitationsConfigParam{Enabled: anthropic.Bool(true)}, }, }, anthropic.NewTextBlock("Summarize this document."), ), }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); DocumentBlockParam documentParam = DocumentBlockParam.builder() .source(UrlPdfSource.builder() .url("https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf") .build()) .title("Document Title") .context("Context about the document that will not be cited from") .citations(CitationsConfigParam.builder().enabled(true).build()) .build(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .addUserMessageOfBlockParams( List.of( ContentBlockParam.ofDocument(documentParam), ContentBlockParam.ofText(TextBlockParam.builder().text("Summarize this document.").build()) ) ) .build(); Message message = client.messages().create(params); System.out.println(message); ``` ```php PHP $client = new Client(); $response = $client->messages->create( maxTokens: 1024, messages: [ [ 'role' => 'user', 'content' => [ [ 'type' => 'document', 'source' => [ 'type' => 'url', 'url' => 'https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf', ], 'title' => 'Document Title', 'context' => 'Context about the document that will not be cited from', 'citations' => ['enabled' => true], ], [ 'type' => 'text', 'text' => 'Summarize this document.', ], ], ], ], model: 'claude-opus-5', ); echo json_encode($response, JSON_PRETTY_PRINT); ``` ```ruby Ruby client = Anthropic::Client.new response = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: [ { type: "document", source: { type: "url", url: "https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf" }, title: "Document Title", context: "Context about the document that will not be cited from", citations: { enabled: true } }, { type: "text", text: "Summarize this document." } ] } ] ) puts response ``` Files API document sources are in beta. These examples use the beta client path; see [Files API](https://platform.claude.com/docs/en/build-with-claude/files) for upload details. ```bash cURL curl -X POST https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: files-api-2025-04-14" \ -H "content-type: application/json" \ -d @- < { new BetaRequestDocumentBlock { Source = new BetaFileDocumentSource { FileID = fileId }, Title = "Document Title", Context = "Context about the document that will not be cited from", Citations = new BetaCitationsConfigParam { Enabled = true }, }, new BetaTextBlockParam { Text = "Summarize this document." }, } } ] }); Console.WriteLine(citedResponse); ``` ```go Go citedMsg, err := client.Beta.Messages.New(context.Background(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Betas: []anthropic.AnthropicBeta{anthropic.AnthropicBetaFilesAPI2025_04_14}, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage( anthropic.BetaContentBlockParamUnion{ OfDocument: &anthropic.BetaRequestDocumentBlockParam{ Source: anthropic.BetaRequestDocumentBlockSourceUnionParam{ OfFile: &anthropic.BetaFileDocumentSourceParam{FileID: fileID}, }, Title: anthropic.String("Document Title"), Context: anthropic.String("Context about the document that will not be cited from"), Citations: anthropic.BetaCitationsConfigParam{Enabled: anthropic.Bool(true)}, }, }, anthropic.NewBetaTextBlock("Summarize this document."), ), }, }) if err != nil { log.Fatal(err) } fmt.Println(citedMsg) ``` ```java Java MessageCreateParams citedParams = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .addBeta("files-api-2025-04-14") .maxTokens(1024) .addUserMessageOfBetaContentBlockParams(List.of( BetaContentBlockParam.ofDocument(BetaRequestDocumentBlock.builder() .source(BetaFileDocumentSource.builder().fileId(fileId).build()) .title("Document Title") .context("Context about the document that will not be cited from") .citations(BetaCitationsConfigParam.builder().enabled(true).build()) .build()), BetaContentBlockParam.ofText(BetaTextBlockParam.builder() .text("Summarize this document.") .build()) )) .build(); BetaMessage citedMessage = client.beta().messages().create(citedParams); System.out.println(citedMessage); ``` ```php PHP $citedResponse = $client->beta->messages->create( maxTokens: 1024, messages: [ [ 'role' => 'user', 'content' => [ [ 'type' => 'document', 'source' => ['type' => 'file', 'file_id' => $fileId], 'title' => 'Document Title', 'context' => 'Context about the document that will not be cited from', 'citations' => ['enabled' => true], ], ['type' => 'text', 'text' => 'Summarize this document.'], ], ], ], model: 'claude-opus-5', betas: ['files-api-2025-04-14'], ); print_r($citedResponse); ``` ```ruby Ruby cited_response = client.beta.messages.create( model: "claude-opus-5", max_tokens: 1024, betas: ["files-api-2025-04-14"], messages: [ { role: "user", content: [ { type: "document", source: { type: "file", file_id: file_id }, title: "Document Title", context: "Context about the document that will not be cited from", citations: { enabled: true } }, { type: "text", text: "Summarize this document." } ] } ] ) puts cited_response ``` ```json { "type": "page_location", "cited_text": "The exact text being cited", // not counted toward output tokens "document_index": 0, "document_title": "Document Title", "start_page_number": 1, // 1-indexed "end_page_number": 2 // exclusive } ``` ### Custom content documents Custom content documents give you control over citation granularity. No additional chunking is done and chunks are provided to the model according to the content blocks provided. ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "messages": [ { "role": "user", "content": [ { "type": "document", "source": { "type": "content", "content": [ {"type": "text", "text": "First chunk"}, {"type": "text", "text": "Second chunk"} ] }, "title": "Document Title", "context": "Context about the document that will not be cited from", "citations": {"enabled": true} }, { "type": "text", "text": "Summarize this document." } ] } ] }' ``` ```bash CLI ant messages create <<'YAML' model: claude-opus-5 max_tokens: 1024 messages: - role: user content: - type: document source: type: content content: - type: text text: First chunk - type: text text: Second chunk title: Document Title context: Context about the document that will not be cited from citations: enabled: true - type: text text: Summarize this document. YAML ``` ```python Python client = anthropic.Anthropic() response = client.messages.create( model="claude-opus-5", max_tokens=1024, messages=[ { "role": "user", "content": [ { "type": "document", "source": { "type": "content", "content": [ {"type": "text", "text": "First chunk"}, {"type": "text", "text": "Second chunk"}, ], }, "title": "Document Title", "context": "Context about the document that will not be cited from", "citations": {"enabled": True}, }, {"type": "text", "text": "Summarize this document."}, ], } ], ) print(response) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: [ { type: "document", source: { type: "content", content: [ { type: "text", text: "First chunk" }, { type: "text", text: "Second chunk" } ] }, title: "Document Title", context: "Context about the document that will not be cited from", citations: { enabled: true } }, { type: "text", text: "Summarize this document." } ] } ] }); console.log(response); ``` ```csharp C# var client = new AnthropicClient(); var response = await client.Messages.Create( new() { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [ new() { Role = Role.User, Content = new MessageParamContent(new List { new ContentBlockParam(new DocumentBlockParam( new DocumentBlockParamSource(new ContentBlockSource() { Content = new ContentBlockSourceContent(new List { new TextBlockParam("First chunk"), new TextBlockParam("Second chunk"), }), }) ) { Title = "Document Title", Context = "Context about the document that will not be cited from", Citations = new CitationsConfigParam { Enabled = true }, }), new ContentBlockParam(new TextBlockParam("Summarize this document.")), }), }, ], } ); Console.WriteLine(response); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage( anthropic.ContentBlockParamUnion{ OfDocument: &anthropic.DocumentBlockParam{ Source: anthropic.DocumentBlockParamSourceUnion{ OfContent: &anthropic.ContentBlockSourceParam{ Content: anthropic.ContentBlockSourceContentUnionParam{ OfContentBlockSourceContent: []anthropic.ContentBlockSourceContentItemUnionParam{ {OfText: &anthropic.TextBlockParam{Text: "First chunk"}}, {OfText: &anthropic.TextBlockParam{Text: "Second chunk"}}, }, }, }, }, Title: anthropic.String("Document Title"), Context: anthropic.String("Context about the document that will not be cited from"), Citations: anthropic.CitationsConfigParam{Enabled: anthropic.Bool(true)}, }, }, anthropic.NewTextBlock("Summarize this document."), ), }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); DocumentBlockParam documentParam = DocumentBlockParam.builder() .source(ContentBlockSource.builder() .contentOfBlockSource( List.of( ContentBlockSourceContent.ofText(TextBlockParam.builder().text("First chunk").build()), ContentBlockSourceContent.ofText(TextBlockParam.builder().text("Second chunk").build()) ) ) .build()) .title("Document Title") .context("Context about the document that will not be cited from") .citations(CitationsConfigParam.builder().enabled(true).build()) .build(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .addUserMessageOfBlockParams( List.of( ContentBlockParam.ofDocument(documentParam), ContentBlockParam.ofText(TextBlockParam.builder().text("Summarize this document.").build()) ) ) .build(); Message message = client.messages().create(params); System.out.println(message); ``` ```php PHP $client = new Client(); $response = $client->messages->create( maxTokens: 1024, messages: [ [ 'role' => 'user', 'content' => [ [ 'type' => 'document', 'source' => [ 'type' => 'content', 'content' => [ ['type' => 'text', 'text' => 'First chunk'], ['type' => 'text', 'text' => 'Second chunk'], ], ], 'title' => 'Document Title', 'context' => 'Context about the document that will not be cited from', 'citations' => ['enabled' => true], ], [ 'type' => 'text', 'text' => 'Summarize this document.', ], ], ], ], model: 'claude-opus-5', ); echo json_encode($response, JSON_PRETTY_PRINT); ``` ```ruby Ruby client = Anthropic::Client.new response = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: [ { type: "document", source: { type: "content", content: [ { type: "text", text: "First chunk" }, { type: "text", text: "Second chunk" } ] }, title: "Document Title", context: "Context about the document that will not be cited from", citations: { enabled: true } }, { type: "text", text: "Summarize this document." } ] } ] ) puts response ``` ```json { "type": "content_block_location", "cited_text": "The exact text being cited", // not counted toward output tokens "document_index": 0, "document_title": "Document Title", "start_block_index": 0, // 0-indexed "end_block_index": 1 // exclusive } ``` *** ## Response structure When citations are enabled, responses include multiple text blocks with citations: ```json { "content": [ { "type": "text", "text": "According to the document, " }, { "type": "text", "text": "the grass is green", "citations": [ { "type": "char_location", "cited_text": "The grass is green.", "document_index": 0, "document_title": "Example Document", "start_char_index": 0, "end_char_index": 20 } ] }, { "type": "text", "text": " and " }, { "type": "text", "text": "the sky is blue", "citations": [ { "type": "char_location", "cited_text": "The sky is blue.", "document_index": 0, "document_title": "Example Document", "start_char_index": 20, "end_char_index": 36 } ] }, { "type": "text", "text": ". Information from page 5 states that " }, { "type": "text", "text": "water is essential", "citations": [ { "type": "page_location", "cited_text": "Water is essential for life.", "document_index": 1, "document_title": "PDF Document", "start_page_number": 5, "end_page_number": 6 } ] }, { "type": "text", "text": ". The custom document mentions " }, { "type": "text", "text": "important findings", "citations": [ { "type": "content_block_location", "cited_text": "These are important findings.", "document_index": 2, "document_title": "Custom Content Document", "start_block_index": 0, "end_block_index": 1 } ] } ] } ``` ### Streaming support For streaming responses, citations arrive as a `citations_delta` delta type inside `content_block_delta` events. Each delta contains a single citation to add to the `citations` list on the current `text` content block. ```sse event: message_start data: {"type": "message_start", ...} event: content_block_start data: {"type": "content_block_start", "index": 0, ...} event: content_block_delta data: {"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "According to..."}} event: content_block_delta data: {"type": "content_block_delta", "index": 0, "delta": {"type": "citations_delta", "citation": { "type": "char_location", "cited_text": "...", "document_index": 0, ... }}} event: content_block_stop data: {"type": "content_block_stop", "index": 0} event: message_stop data: {"type": "message_stop"} ``` ## Next steps Handle the `citations_delta` delta type alongside text deltas to render cited responses as they stream. Pass search results from your RAG pipeline as first-class content blocks with built-in citation support. Learn how Claude extracts text from PDFs and how page-based citations map back to your source files. Upload documents once and reference them by `file_id` across multiple citation requests. --- title: Effort url: https://platform.claude.com/docs/en/build-with-claude/effort description: Control how many tokens Claude uses when responding with the effort parameter, trading off between response thoroughness and token efficiency. --- ## Compatibility - [ZDR](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention): eligible (excludes [Covered Models](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#model-specific-data-retention-requirements)) - Supported models: `claude-fable-5`, `claude-mythos-5`, `claude-mythos-preview`, `claude-opus-5`, `claude-opus-4-8`, `claude-opus-4-7`, `claude-opus-4-6`, `claude-sonnet-5`, `claude-sonnet-4-6`, `claude-opus-4-5-20251101` - Platforms: Claude API, Claude Platform on AWS, Amazon Bedrock, Google Cloud, Microsoft Foundry The effort parameter lets you control how many tokens Claude spends when responding to requests. You can trade off between response thoroughness and token efficiency with a single model. The effort parameter is available on all supported models with no beta header required. For how effort interacts with thinking and which control to reach for, see [Thinking and effort](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-and-effort). Where adaptive thinking is available, effort is the recommended way to control thinking depth. ## How effort works By default, Claude uses high effort, spending as many tokens as needed for excellent results. You can raise the effort level to `max` for the absolute highest capability, or lower it to be more conservative with token usage, optimizing for speed and cost while accepting some reduction in capability. Setting `effort` to `"high"` produces exactly the same behavior as omitting the `effort` parameter entirely. The effort parameter affects **all tokens** in the response, including: * Text responses and explanations * Tool calls and function arguments * Thinking (when active) This approach has two major advantages: 1. It doesn't require thinking to be enabled. 2. It can affect all token spend including tool calls. For example, lower effort would mean Claude makes fewer tool calls. This gives a much greater degree of control over efficiency. ### Effort levels | Level | Description | Typical use case | | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | | `max` | Absolute maximum capability with no constraints on token spending. Available on Claude Fable 5, Claude Mythos 5, Claude Opus 5, Claude Opus 4.8, Claude Mythos Preview, Claude Opus 4.7, Claude Opus 4.6, Claude Sonnet 5, and Claude Sonnet 4.6. | Tasks requiring the deepest possible reasoning and most thorough analysis | | `xhigh` | Extended capability for long-horizon work. Available on Claude Fable 5, Claude Mythos 5, Claude Opus 5, Claude Opus 4.8, Claude Opus 4.7, and Claude Sonnet 5. | Long-running agentic and coding tasks (over 30 minutes) with token budgets in the millions | | `high` | High capability. Equivalent to not setting the parameter. | Complex reasoning, difficult coding problems, agentic tasks | | `medium` | Balanced approach with moderate token savings. | Agentic tasks that require a balance of speed, cost, and performance | | `low` | Most efficient. Significant token savings with some capability reduction. | Simpler tasks that need the best speed and lowest costs, such as subagents | `xhigh` is a newer level; some models that support `max` don't support `xhigh`. Effort is a behavioral signal, not a strict token budget. At lower effort levels, Claude will still think on sufficiently difficult problems, but it will think less than it would at higher effort levels for the same problem. ### Recommended effort levels for Claude Sonnet 5 Claude Sonnet 5 defaults to `high` effort on the Claude API and Claude Code. * **High effort (default):** Suitable for complex reasoning, coding, and agentic tasks where quality matters more than speed or cost. * **Xhigh effort:** For the hardest coding and agentic tasks. See [Prompting Claude Sonnet 5](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/prompting-claude-sonnet-5#calibrating-effort-and-thinking-depth). * **Medium effort:** Cost-saving step-down from the default. Comparable to Claude Sonnet 4.6 at high effort. * **Low effort:** For high-volume or latency-sensitive workloads. Suitable for chat and non-coding use cases where faster turnaround is prioritized. * **Max effort:** For tasks requiring the absolute highest capability with no constraints on token spending. ### Recommended effort levels for Claude Sonnet 4.6 Sonnet 4.6 defaults to `high` effort. Explicitly set effort when using Sonnet 4.6 to avoid unexpected latency: * **Medium effort** (recommended default): Best balance of speed, cost, and performance for most applications. Suitable for agentic coding, tool-heavy workflows, and code generation. * **Low effort:** For high-volume or latency-sensitive workloads. Suitable for chat and non-coding use cases where faster turnaround is prioritized. * **High effort:** For complex reasoning and tasks where quality matters more than speed or cost. * **Max effort:** For tasks requiring the absolute highest capability with no constraints on token spending. ### Recommended effort levels for Claude Opus 4.7 **Start with `xhigh` for coding and agentic use cases**, and use `high` as the minimum for most intelligence-sensitive workloads. Step down to `medium` for cost-sensitive workloads, or up to `max` only when your evals show measurable headroom at `xhigh`. The API default is `high`. To use `xhigh`, set `effort` explicitly; the value you pass overrides the default. | Effort | Guidance for Claude Opus 4.7 | | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `low` | Efficient, but best for short, scoped tasks. Pair `low` with explicit checklists if your task has multiple sections. | | `medium` | The drop-in for the average workflow where you want good results while reducing costs. | | `high` | Advanced use cases that still need a balance of intelligence and token consumption. This is often the best balance of quality and token efficiency. | | `xhigh` | The recommended starting point for coding and agentic work, and for exploratory tasks such as repeated tool calling, detailed web search, and knowledge-base search. Expect meaningfully higher token usage than `high`. | | `max` | Reserve for genuinely frontier problems. On most workloads `max` adds significant cost for relatively small quality gains, and on some structured-output or less intelligence-sensitive tasks it can lead to overthinking. | Claude Opus 4.7 also respects effort levels more strictly than Claude Opus 4.6, especially at `low` and `medium`. At lower effort levels, the model scopes its work to what was asked rather than doing more than requested. If you observe shallow reasoning on complex problems with Claude Opus 4.7, raise effort rather than prompting around it. If you must keep effort low for latency, add targeted guidance like "This task involves multistep reasoning. Think carefully before responding." When running Claude Opus 4.7 at `xhigh` or `max` effort, set a large `max_tokens` so the model has room to think and act across subagents and tool calls. Starting at 64k tokens and tuning from there is a reasonable default. ### Recommended effort levels for Claude Opus 4.8 The guidance for Claude Opus 4.7 also applies to Claude Opus 4.8. **Start with `xhigh` for coding and agentic use cases**, use `high` for most other intelligence-sensitive workloads, and step down to `medium` or `low` only when you've measured that the lower level holds quality on your evals. The API default is `high`. Set `effort` explicitly to use a different level; the value you pass overrides the default. When running Claude Opus 4.8 at `xhigh` or `max` effort, set a large `max_tokens` so the model has room to think and act across subagents and tool calls. Starting at 64k tokens and tuning from there is a reasonable default. ### Recommended effort levels for Claude Opus 5 Claude Opus 5 supports all five effort levels. **Start with `high`, the default**, and adjust based on your evals: step up to `xhigh` for demanding coding and agentic work, or to `max` when a task justifies unconstrained token spending, and use `low` and `medium` liberally as your primary control for token cost and response time wherever your evals show quality holds. If you carried effort settings over from an earlier model, run a fresh effort sweep on your evals rather than reusing them. Effort controls thinking volume, not visible response length: on Claude Opus 5, changing effort does not reliably shorten responses, so [prompt for length](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/prompting-claude-opus-5#response-length-and-verbosity) instead. The API default is `high`. Set `effort` explicitly to use a different level; the value you pass overrides the default. On Claude Opus 5, thinking cannot be disabled at `xhigh` or `max` effort: requests that set `thinking: {"type": "disabled"}` at those levels return a 400 error. See [Effort with thinking](https://platform.claude.com/docs/en/build-with-claude/effort#effort-with-thinking). When running Claude Opus 5 at `xhigh` or `max` effort, set a large `max_tokens` so the model has room to think and act across subagents and tool calls. Starting at 64k tokens and tuning from there is a reasonable default. ### Recommended effort levels for Claude Fable 5 Effort is the primary control for trading off intelligence, latency, and cost on Claude Fable 5. **Start with `high`, the default, for most tasks**, use `xhigh` for the most capability-sensitive workloads, and step down to `medium` or `low` for routine work. Lower effort settings on Claude Fable 5 still perform well and often exceed `xhigh` performance on prior models. At `high` and `xhigh`, set a large `max_tokens`: it is a hard limit on total output, thinking plus response text. See [Cost control](https://platform.claude.com/docs/en/build-with-claude/thinking-steering-and-cost#cost-control). Reduce effort if a task completes but takes longer than necessary, or if you want a faster, more interactive working style. The same recommendations apply to Claude Mythos 5. For fuller guidance, see [Prompting Claude Fable 5](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/prompting-claude-fable-5). ## Basic usage ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 4096, "messages": [{ "role": "user", "content": "Analyze the trade-offs between microservices and monolithic architectures" }], "output_config": { "effort": "medium" } }' ``` ```bash CLI ant messages create \ --model claude-opus-5 \ --max-tokens 4096 \ --output-config '{effort: medium}' \ --message '{role: user, content: "Analyze the trade-offs between microservices and monolithic architectures"}' \ --transform 'content.#(type=="text").text' \ --raw-output ``` ```python Python client = anthropic.Anthropic() response = client.messages.create( model="claude-opus-5", max_tokens=4096, messages=[ { "role": "user", "content": "Analyze the trade-offs between microservices and monolithic architectures", } ], output_config={"effort": "medium"}, ) for block in response.content: if block.type == "text": print(block.text) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 4096, messages: [ { role: "user", content: "Analyze the trade-offs between microservices and monolithic architectures" } ], output_config: { effort: "medium" } }); const textBlock = response.content.find( (block): block is Anthropic.TextBlock => block.type === "text" ); console.log(textBlock?.text); ``` ```csharp C# AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 4096, Messages = [ new() { Role = Role.User, Content = "Analyze the trade-offs between microservices and monolithic architectures" } ], OutputConfig = new OutputConfig { Effort = Effort.Medium } }; var message = await client.Messages.Create(parameters); Console.WriteLine(message); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 4096, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Analyze the trade-offs between microservices and monolithic architectures")), }, OutputConfig: anthropic.OutputConfigParam{ Effort: anthropic.OutputConfigEffortMedium, }, }) if err != nil { log.Fatal(err) } for _, block := range response.Content { if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok { fmt.Println(textBlock.Text) } } ``` ```java Java import com.anthropic.models.messages.OutputConfig; void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(4096L) .addUserMessage("Analyze the trade-offs between microservices and monolithic architectures") .outputConfig(OutputConfig.builder() .effort(OutputConfig.Effort.MEDIUM) .build()) .build(); Message response = client.messages().create(params); response.content().stream() .flatMap(block -> block.text().stream()) .forEach(textBlock -> IO.println(textBlock.text())); } ``` ```php PHP $client = new Client(); $message = $client->messages->create( maxTokens: 4096, messages: [ ['role' => 'user', 'content' => 'Analyze the trade-offs between microservices and monolithic architectures'] ], model: 'claude-opus-5', outputConfig: ['effort' => 'medium'], ); foreach ($message->content as $block) { if ($block->type === 'text') { echo $block->text, PHP_EOL; } } ``` ```ruby Ruby client = Anthropic::Client.new message = client.messages.create( model: "claude-opus-5", max_tokens: 4096, messages: [ { role: "user", content: "Analyze the trade-offs between microservices and monolithic architectures" } ], output_config: { effort: "medium" } ) message.content.each do |block| puts block.text if block.type == :text end ``` ## When to adjust the effort parameter * Use **max effort** when you need the absolute highest capability with no constraints: the most thorough reasoning and deepest analysis. Available on Claude 4.6 and later models and Claude Mythos Preview. * Use **xhigh effort** for advanced coding and complex agentic work requiring extended exploration, such as repeated tool calling and detailed search. Available on Claude Fable 5, Claude Mythos 5, Claude Opus 5, Claude Opus 4.8, Claude Opus 4.7, and Claude Sonnet 5. * Use **high effort** (the default) for complex reasoning, nuanced analysis, difficult coding problems, or any task where quality matters more than speed or cost. * Use **medium effort** as a balanced option when you want solid performance without the full token expenditure of high effort. * Use **low effort** when you're optimizing for speed (because Claude answers with fewer tokens) or cost. For example, simple classification tasks, quick lookups, or high-volume use cases where marginal quality improvements don't justify additional latency or spend. ## Effort with tool use When using tools, the effort parameter affects both the explanations around tool calls and the tool calls themselves. Lower effort levels tend to: * Combine multiple operations into fewer tool calls * Make fewer tool calls * Proceed directly to action without preamble * Use terse confirmation messages after completion Higher effort levels may: * Make more tool calls * Explain the plan before taking action * Provide detailed summaries of changes * Include more comprehensive code comments ## Effort with thinking The `thinking` parameter controls whether Claude thinks in [thinking blocks](https://platform.claude.com/docs/en/build-with-claude/thinking) before answering; the `effort` parameter controls how much work Claude puts into the whole response, which in adaptive mode includes how often and how deeply it thinks. Don't pass `adaptive` as an `effort` value: `adaptive` is a thinking mode, not an effort level. At higher effort levels, Claude thinks on most requests and at greater length; at lower levels, it can skip thinking entirely for simpler problems. See [Thinking and effort](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-and-effort) for full guidance on how the two controls work together. On Claude Opus 4.5, the only extended-thinking-only model that supports effort, it works alongside [`budget_tokens`](https://platform.claude.com/docs/en/build-with-claude/extended-thinking): set the effort level for your task, then set the thinking token budget based on how much reasoning depth the task needs. For per-model thinking availability, see the [per-model configuration table](https://platform.claude.com/docs/en/build-with-claude/thinking-troubleshooting#supported-models). Effort works with or without thinking; see [How effort works](https://platform.claude.com/docs/en/build-with-claude/effort#how-effort-works). ## Changing effort mid-conversation `output_config.effort` is a request-level setting: each request carries its own value, so to run a later part of a conversation at a different effort level, set the new value on the next request. The effort level applies to the whole request. Because effort shapes the rendered prompt, changing it between requests does not preserve cached prefixes from earlier turns; if you rely on [prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) across a long session, pick an effort level at the start and keep it constant. ## Best practices 1. **Set effort explicitly:** The API defaults to `high`, but the right starting point depends on your model and workload. 2. **Use low for speed-sensitive or simple tasks:** When latency matters or tasks are straightforward, low effort can significantly reduce response times and costs. 3. **Test your use case:** The impact of effort levels varies by task type. Evaluate performance on your specific use cases before deploying. 4. **Consider dynamic effort:** Adjust effort based on task complexity. Simple queries may warrant low effort while agentic coding and complex reasoning benefit from high effort. See the next item before varying it within one conversation. 5. **Hold effort constant within cached conversations:** Changing the effort value between requests invalidates [prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching), so vary effort across workloads rather than within a conversation that relies on cache hits. See [Thinking and prompt caching](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-and-prompt-caching). ## Next steps Give Claude an advisory token budget for the full agentic loop to help the model self-regulate on long agentic tasks. Understand adaptive thinking, where Claude decides when and how much to think, and steer it with effort and prompting. Understand how thinking works, when Claude thinks by default, and how thinking interacts with effort. --- title: Embeddings url: https://platform.claude.com/docs/en/build-with-claude/embeddings description: Text embeddings are numerical representations of text that enable measuring semantic similarity. This guide introduces embeddings, their applications, and how to use embedding models for tasks like search, recommendations, and anomaly detection. --- ## Before implementing embeddings When selecting an embeddings provider, there are several factors you can consider depending on your needs and preferences: * Dataset size & domain specificity: size of the model training dataset and its relevance to the domain you want to embed. Larger or more domain-specific data generally produces better in-domain embeddings * Inference performance: embedding lookup speed and end-to-end latency. This is a particularly important consideration for large scale production deployments * Customization: options for continued training on private data, or specialization of models for very specific domains. This can improve performance on unique vocabularies ## How to get embeddings with Anthropic Anthropic does not offer its own embedding model. One embeddings provider that has a wide variety of options and capabilities encompassing all of the preceding considerations is Voyage AI. Voyage AI makes state-of-the-art embedding models and offers customized models for specific industry domains such as finance and healthcare, or bespoke fine-tuned models for individual customers. The rest of this guide is for Voyage AI, but you should assess a variety of embeddings vendors to find the best fit for your specific use case. ## Available models Voyage recommends using the following text embedding models: **Voyage 4 (latest generation)** | Model | Context length | Embedding dimension | Description | | ---------------- | -------------- | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `voyage-4-large` | 32,000 | 1024 (default), 256, 512, 2048 | The best general-purpose and multilingual retrieval quality. See the [Voyage 4 blog post](https://blog.voyageai.com/2026/01/15/voyage-4/) for details. | | `voyage-4` | 32,000 | 1024 (default), 256, 512, 2048 | Optimized for general-purpose and multilingual retrieval quality. Balances quality and efficiency. See the [Voyage 4 blog post](https://blog.voyageai.com/2026/01/15/voyage-4/) for details. | | `voyage-4-lite` | 32,000 | 1024 (default), 256, 512, 2048 | Optimized for latency and cost. See the [Voyage 4 blog post](https://blog.voyageai.com/2026/01/15/voyage-4/) for details. | | `voyage-4-nano` | 32,000 | 1024 (default), 256, 512, 2048 | Open-weight model (Apache 2.0 license) available on Hugging Face. See the [Voyage 4 blog post](https://blog.voyageai.com/2026/01/15/voyage-4/) for details. | **Previous generation** | Model | Context length | Embedding dimension | Description | | ------------------ | -------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `voyage-3-large` | 32,000 | 1024 (default), 256, 512, 2048 | The best general-purpose and multilingual retrieval quality. See the [voyage-3-large blog post](https://blog.voyageai.com/2025/01/07/voyage-3-large/) for details. | | `voyage-3.5` | 32,000 | 1024 (default), 256, 512, 2048 | Optimized for general-purpose and multilingual retrieval quality. See the [voyage-3.5 blog post](https://blog.voyageai.com/2025/05/20/voyage-3-5/) for details. | | `voyage-3.5-lite` | 32,000 | 1024 (default), 256, 512, 2048 | Optimized for latency and cost. See the [voyage-3.5 blog post](https://blog.voyageai.com/2025/05/20/voyage-3-5/) for details. | | `voyage-code-3` | 32,000 | 1024 (default), 256, 512, 2048 | Optimized for **code** retrieval. See the [voyage-code-3 blog post](https://blog.voyageai.com/2024/12/04/voyage-code-3/) for details. | | `voyage-finance-2` | 32,000 | 1024 | Optimized for **finance** retrieval and RAG. See the [voyage-finance-2 blog post](https://blog.voyageai.com/2024/06/03/domain-specific-embeddings-finance-edition-voyage-finance-2/) for details. | | `voyage-law-2` | 16,000 | 1024 | Optimized for **legal** and **long-context** retrieval and RAG. Also improved performance across all domains. See the [voyage-law-2 blog post](https://blog.voyageai.com/2024/04/15/domain-specific-embeddings-and-retrieval-legal-edition-voyage-law-2/) for details. | Additionally, Voyage recommends the following multimodal embedding models: | Model | Context length | Embedding dimension | Description | | ----------------------- | -------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `voyage-multimodal-3.5` | 32,000 | 1024 (default), 256, 512, 2048 | Rich multimodal embedding model that can vectorize interleaved text, images, and videos. Includes video support as the first production-grade video embedding model. See the [voyage-multimodal-3.5 blog post](https://blog.voyageai.com/2026/01/15/voyage-multimodal-3-5/) for details. | | `voyage-multimodal-3` | 32,000 | 1024 | Rich multimodal embedding model that can vectorize interleaved text and content-rich images, such as screenshots of PDFs, slides, tables, figures, and more. See the [voyage-multimodal-3 blog post](https://blog.voyageai.com/2024/11/12/voyage-multimodal-3/) for details. | The following contextualized chunk embedding models produce chunk-level vectors that capture full document context without manual metadata augmentation. Call these models with `contextualized_embed()` instead of `embed()`: | Model | Context length | Embedding dimension | Description | | ------------------ | -------------- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `voyage-context-4` | 120,000 | 1024 (default), 256, 512, 2048 | Contextualized chunk embeddings optimized for general-purpose and multilingual retrieval quality. See the [voyage-context-4 blog post](https://blog.voyageai.com/2026/06/29/voyage-context-4/) for details. | | `voyage-context-3` | 120,000 | 1024 (default), 256, 512, 2048 | Contextualized chunk embeddings optimized for general-purpose and multilingual retrieval quality. See the [voyage-context-3 blog post](https://blog.voyageai.com/2025/07/23/voyage-context-3/) for details. | Voyage AI also offers rerankers, which take a query and a list of documents and return them ranked by relevance to the query. Call these models with `rerank()`: | Model | Context length | Description | | ----------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `rerank-2.5` | 32,000 | Highest accuracy. Recommended for most applications. See the [rerank-2.5 blog post](https://blog.voyageai.com/2025/08/11/rerank-2-5/) for details. | | `rerank-2.5-lite` | 32,000 | Optimized for latency and cost. See the [rerank-2.5 blog post](https://blog.voyageai.com/2025/08/11/rerank-2-5/) for details. | Need help deciding which text embedding model to use? Check out the [Voyage AI FAQ](https://docs.voyageai.com/docs/faq#what-embedding-models-are-available-and-which-one-should-i-use\&ref=anthropic). ## Getting started with Voyage AI To access Voyage embeddings: 1. Sign up on Voyage AI's website. 2. Obtain an API key. 3. Set the API key as an environment variable for convenience: ```bash export VOYAGE_API_KEY="" ``` You can obtain the embeddings by either using the official [`voyageai` Python package](https://github.com/voyage-ai/voyageai-python) or HTTP requests, as described in the following sections. ### Voyage Python library Install the `voyageai` package using the following command: ```bash pip install -U voyageai ``` Then, you can create a client object and start using it to embed your texts: ```python import voyageai vo = voyageai.Client() # This will automatically use the environment variable VOYAGE_API_KEY. # Alternatively, you can use vo = voyageai.Client(api_key="") texts = ["Sample text 1", "Sample text 2"] result = vo.embed(texts, model="voyage-4", input_type="document") print(result.embeddings[0]) print(result.embeddings[1]) ``` `result.embeddings` is a list of two embedding vectors, each containing 1024 floating-point numbers. After running the preceding code, the two embeddings are printed on the screen: ```text [-0.013131560757756233, 0.019828535616397858, ...] # embedding for "Sample text 1" [-0.0069352793507277966, 0.020878976210951805, ...] # embedding for "Sample text 2" ``` When creating the embeddings, you can specify a few other arguments to the `embed()` function. For more information on the Voyage Python package, see the [Voyage Python package documentation](https://docs.voyageai.com/docs/embeddings#python-api). ### Voyage HTTP API You can also get embeddings by requesting Voyage HTTP API. For example, you can send an HTTP request through the `curl` command in a terminal: ```bash cURL curl https://api.voyageai.com/v1/embeddings \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $VOYAGE_API_KEY" \ -d '{ "input": ["Sample text 1", "Sample text 2"], "model": "voyage-4" }' ``` The response you would get is a JSON object containing the embeddings and the token usage: ```json { "object": "list", "data": [ { "embedding": [-0.013131560757756233, 0.019828535616397858 /* ... */], "index": 0 }, { "embedding": [-0.0069352793507277966, 0.020878976210951805 /* ... */], "index": 1 } ], "model": "voyage-4", "usage": { "total_tokens": 10 } } ``` For more information on the Voyage HTTP API, see the [Voyage HTTP API documentation](https://docs.voyageai.com/reference/embeddings-api). ### AWS Marketplace Voyage embeddings are available on [AWS Marketplace](https://aws.amazon.com/marketplace/seller-profile?id=c9032c7b-70dd-459f-834f-c1e23cf3d092). Instructions for accessing Voyage on AWS are available in the [Voyage AWS Marketplace documentation](https://docs.voyageai.com/docs/aws-marketplace-mongodb-voyage?ref=anthropic). ## Quickstart example The following brief example shows how to use embeddings. Suppose you have a small corpus of six documents to retrieve from ```python documents = [ "The Mediterranean diet emphasizes fish, olive oil, and vegetables, believed to reduce chronic diseases.", "Photosynthesis in plants converts light energy into glucose and produces essential oxygen.", "20th-century innovations, from radios to smartphones, centered on electronic advancements.", "Rivers provide water, irrigation, and habitat for aquatic species, vital for ecosystems.", "Apple's conference call to discuss fourth fiscal quarter results and business updates is scheduled for Thursday, November 2, 2023 at 2:00 p.m. PT / 5:00 p.m. ET.", "Shakespeare's works, like 'Hamlet' and 'A Midsummer Night's Dream,' endure in literature.", ] ``` First, use Voyage to convert each document into an embedding vector. ```python import voyageai vo = voyageai.Client() # Embed the documents doc_embds = vo.embed(documents, model="voyage-4", input_type="document").embeddings ``` The embeddings allow you to do semantic search / retrieval in the vector space. Given an example query, ```python query = "When is Apple's conference call scheduled?" ``` Next, convert it into an embedding and conduct a nearest neighbor search to find the most relevant document based on the distance in the embedding space. ```python import numpy as np # Embed the query query_embd = vo.embed([query], model="voyage-4", input_type="query").embeddings[0] # Compute the similarity # Voyage embeddings are normalized to length 1, therefore dot-product # and cosine similarity are the same. similarities = np.dot(doc_embds, query_embd) retrieved_id = np.argmax(similarities) print(documents[retrieved_id]) ``` Note that `input_type="document"` and `input_type="query"` are used for embedding the document and query, respectively. More specification can be found in [Voyage Python library](https://platform.claude.com/docs/en/build-with-claude/embeddings#voyage-python-library). The output is the fifth document, which is indeed the most relevant to the query: ```text wrap Apple's conference call to discuss fourth fiscal quarter results and business updates is scheduled for Thursday, November 2, 2023 at 2:00 p.m. PT / 5:00 p.m. ET. ``` If you are looking for a detailed set of recipes on how to do RAG with embeddings, including vector databases, check out the [RAG recipe](https://platform.claude.com/cookbook/third-party-pinecone-rag-using-pinecone). ## FAQ Embedding models rely on powerful neural networks to capture and compress semantic context, similar to generative models. Voyage's team of experienced AI researchers optimizes every component of the embedding process, including: * Model architecture * Data collection * Loss functions * Optimizer selection Learn more about Voyage's technical approach on the [Voyage AI blog](https://blog.voyageai.com/). For general-purpose embedding, the recommended models are: * `voyage-4-large`: Best quality * `voyage-4-lite`: Lowest latency and cost * `voyage-4`: Balanced performance For retrieval, use the `input_type` parameter to specify whether the text is a query or document type. Domain-specific models: * Legal tasks: `voyage-law-2` * Code and programming documentation: `voyage-code-3` * Finance-related tasks: `voyage-finance-2` For chunk-level and document-level retrieval: `voyage-context-4` You can use Voyage embeddings with either dot-product similarity, cosine similarity, or Euclidean distance. For an explanation of embedding similarity, see this [vector similarity guide](https://www.pinecone.io/learn/vector-similarity/). Voyage AI embeddings are normalized to length 1, which means that: * Cosine similarity is equivalent to dot-product similarity, while the latter can be computed more quickly. * Cosine similarity and Euclidean distance result in identical rankings. See the [Voyage tokenization guide](https://docs.voyageai.com/docs/tokenization?ref=anthropic). For all retrieval tasks and use cases (for example, RAG), use the `input_type` parameter to specify whether the input text is a query or document. Do not omit `input_type` or set `input_type=None`. Specifying whether input text is a query or document can create better dense vector representations for retrieval, which can lead to better retrieval quality. When using the `input_type` parameter, special prompts are prepended to the input text prior to embedding. Specifically: > 📘 **Prompts associated with `input_type`** > > * For a query, the prompt is “Represent the query for retrieving supporting documents: “. > > * For a document, the prompt is “Represent the document for retrieval: “. > > * Example > > * When `input_type="query"`, a query like "When is Apple's conference call scheduled?" will become "**Represent the query for retrieving supporting documents:** When is Apple's conference call scheduled?" > > * When `input_type="document"`, a query like "Apple's conference call to discuss fourth fiscal quarter results and business updates is scheduled for Thursday, November 2, 2023 at 2p.m. PT / 5p.m. ET." will become "**Represent the document for retrieval:** Apple's conference call to discuss fourth fiscal quarter results and business updates is scheduled for Thursday, November 2, 2023 at 2p.m. PT / 5p.m. ET." `voyage-large-2-instruct`, as the name suggests, is trained to be responsive to additional instructions that are prepended to the input text. For classification, clustering, or other [MTEB](https://huggingface.co/mteb) subtasks, use the [voyage-large-2-instruct instructions](https://github.com/voyage-ai/voyage-large-2-instruct). Quantization in embeddings converts high-precision values, such as 32-bit single-precision floating-point numbers, to lower-precision formats such as 8-bit integers or 1-bit binary values, reducing storage, memory, and costs by 4x and 32x, respectively. Supported Voyage models enable quantization by specifying the output data type with the `output_dtype` parameter: * `float`: Each returned embedding is a list of 32-bit (4-byte) single-precision floating-point numbers. This is the default and provides the highest precision / retrieval accuracy. * `int8` and `uint8`: Each returned embedding is a list of 8-bit (1-byte) integers ranging from -128 to 127 and 0 to 255, respectively. * `binary` and `ubinary`: Each returned embedding is a list of 8-bit integers that represent bit-packed, quantized single-bit embedding values: `int8` for `binary` and `uint8` for `ubinary`. The length of the returned list of integers is 1/8 of the actual dimension of the embedding. The binary type uses the offset binary method, which you can learn more about in the [embeddings FAQ](https://platform.claude.com/docs/en/build-with-claude/embeddings#faq). > **Binary quantization example** > > Consider the following eight embedding values: -0.03955078, 0.006214142, -0.07446289, -0.039001465, 0.0046463013, 0.00030612946, -0.08496094, and 0.03994751. With binary quantization, values less than or equal to zero will be quantized to a binary zero, and positive values to a binary one, resulting in the following binary sequence: 0, 1, 0, 0, 1, 1, 0, 1. These eight bits are then packed into a single 8-bit integer, 01001101 (with the leftmost bit as the most significant bit). > > * `ubinary`: The binary sequence is directly converted and represented as the unsigned integer (`uint8`) 77. > * `binary`: The binary sequence is represented as the signed integer (`int8`) -51, calculated using the offset binary method (77 - 128 = -51). Matryoshka learning creates embeddings with coarse-to-fine representations within a single vector. Voyage models, such as `voyage-code-3`, that support multiple output dimensions generate such Matryoshka embeddings. You can truncate these vectors by keeping the leading subset of dimensions. For example, the following Python code demonstrates how to truncate 1024-dimensional vectors to 256 dimensions: ```python import voyageai import numpy as np def embd_normalize(v: np.ndarray) -> np.ndarray: """ Normalize the rows of a 2D numpy array to unit vectors by dividing each row by its Euclidean norm. Raises a ValueError if any row has a norm of zero to prevent division by zero. """ row_norms = np.linalg.norm(v, axis=1, keepdims=True) if np.any(row_norms == 0): raise ValueError("Cannot normalize rows with a norm of zero.") return v / row_norms vo = voyageai.Client() # Generate voyage-code-3 vectors, which by default are 1024-dimensional floating-point numbers embd = vo.embed(["Sample text 1", "Sample text 2"], model="voyage-code-3").embeddings # Set shorter dimension short_dim = 256 # Resize and normalize vectors to shorter dimension resized_embd = embd_normalize(np.array(embd)[:, :short_dim]).tolist() ``` ## Pricing Visit Voyage's [pricing page](https://docs.voyageai.com/docs/pricing?ref=anthropic) for the most up to date pricing details. --- title: Fast mode (research preview) url: https://platform.claude.com/docs/en/build-with-claude/fast-mode description: Get up to 2.5x higher output tokens per second from supported Claude Opus models. --- Fast mode delivers up to 2.5x higher output tokens per second from Claude Opus 5 and Claude Opus 4.8 at premium pricing. Set `speed: "fast"` with the `fast-mode-2026-02-01` beta header on your request to opt in. Fast mode is in research preview. Contact your account manager to request access. If you do not have an account manager, [join the waitlist](https://claude.com/fast-mode) for fast mode. For how zero data retention (ZDR) applies to this feature, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention). ## Supported models Fast mode is supported on the following models: * Claude Opus 5 (claude-opus-5) * Claude Opus 4.8 (claude-opus-4-8) Fast mode for Claude Opus 5 and Claude Opus 4.8 is available as a research preview on the Claude API, including [Claude Managed Agents](https://platform.claude.com/docs/en/managed-agents/overview), only. It is not available on Amazon Bedrock, Google Cloud, or Microsoft Foundry. Fast mode is not available on Claude Opus 4.7. Requests to `claude-opus-4-7` with `speed: "fast"` return an error; unlike Claude Opus 4.6 (see the following note), requests do not fall back to standard speed. The model itself remains available at standard speed. To continue using fast mode, migrate to [Claude Opus 5](https://platform.claude.com/docs/en/about-claude/models/migration-guide#migrating-from-claude-opus-47) or Claude Opus 4.8. Fast mode is not available on Claude Opus 4.6. Requests to `claude-opus-4-6` with `speed: "fast"` do not return an error: they run at standard speed and are billed at [standard rates](https://platform.claude.com/docs/en/about-claude/pricing) rather than fast mode's premium rates, and the response reports [`usage.speed: "standard"`](https://platform.claude.com/docs/en/build-with-claude/fast-mode#checking-which-speed-was-used). To continue using fast mode, migrate to [Claude Opus 5](https://platform.claude.com/docs/en/about-claude/models/migration-guide#migrating-from-claude-opus-46) or Claude Opus 4.8. ## How fast mode works Fast mode runs the same model with a faster inference configuration. There is no change to intelligence or capabilities. * Up to 2.5x higher output tokens per second compared to standard speed * Speed benefits are focused on output tokens per second (OTPS), not time to first token (TTFT) * Same model weights and behavior (not a different model) * Compatible with [streaming](https://platform.claude.com/docs/en/build-with-claude/streaming), where the OTPS gain is most visible ## Basic usage ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: fast-mode-2026-02-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 4096, "speed": "fast", "messages": [{ "role": "user", "content": "Refactor this module to use dependency injection" }] }' ``` ```bash CLI ant beta:messages create \ --beta fast-mode-2026-02-01 \ --transform 'content.#(type=="text").text' \ --raw-output <<'YAML' model: claude-opus-5 max_tokens: 4096 speed: fast messages: - role: user content: Refactor this module to use dependency injection YAML ``` ```python Python client = anthropic.Anthropic() response = client.beta.messages.create( model="claude-opus-5", max_tokens=4096, speed="fast", betas=["fast-mode-2026-02-01"], messages=[ {"role": "user", "content": "Refactor this module to use dependency injection"} ], ) for block in response.content: if block.type == "text": print(block.text) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.beta.messages.create({ model: "claude-opus-5", max_tokens: 4096, speed: "fast", betas: ["fast-mode-2026-02-01"], messages: [ { role: "user", content: "Refactor this module to use dependency injection" } ] }); const textBlock = response.content.find( (block): block is Anthropic.Beta.Messages.BetaTextBlock => block.type === "text" ); console.log(textBlock?.text); ``` ```csharp C# AnthropicClient client = new(); var response = await client.Beta.Messages.Create(new MessageCreateParams { Model = "claude-opus-5", MaxTokens = 4096, Speed = Speed.Fast, Betas = ["fast-mode-2026-02-01"], Messages = [ new() { Role = Role.User, Content = "Refactor this module to use dependency injection" } ], }); foreach (var block in response.Content) { if (block.TryPickText(out var textBlock)) { Console.WriteLine(textBlock.Text); } } ``` ```go Go client := anthropic.NewClient() response, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 4096, Speed: anthropic.BetaMessageNewParamsSpeedFast, Betas: []anthropic.AnthropicBeta{anthropic.AnthropicBetaFastMode2026_02_01}, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Refactor this module to use dependency injection")), }, }) if err != nil { log.Fatal(err) } for _, block := range response.Content { if textBlock, ok := block.AsAny().(anthropic.BetaTextBlock); ok { fmt.Println(textBlock.Text) } } ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); BetaMessage response = client.beta().messages().create( MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(4096L) .speed(MessageCreateParams.Speed.FAST) .addBeta(AnthropicBeta.FAST_MODE_2026_02_01) .addUserMessage("Refactor this module to use dependency injection") .build()); response.content().stream() .flatMap(block -> block.text().stream()) .forEach(textBlock -> IO.println(textBlock.text())); ``` ```php PHP $client = new Client(); $response = $client->beta->messages->create( model: 'claude-opus-5', maxTokens: 4096, speed: 'fast', betas: ['fast-mode-2026-02-01'], messages: [ ['role' => 'user', 'content' => 'Refactor this module to use dependency injection'], ], ); foreach ($response->content as $block) { if ($block->type === 'text') { echo $block->text, PHP_EOL; } } ``` ```ruby Ruby client = Anthropic::Client.new response = client.beta.messages.create( model: "claude-opus-5", max_tokens: 4096, speed: "fast", betas: ["fast-mode-2026-02-01"], messages: [{role: "user", content: "Refactor this module to use dependency injection"}] ) response.content.each do |block| puts block.text if block.type == :text end ``` ## Pricing Fast mode is priced at a multiplier on standard rates across the full context window, including requests over 200k input tokens. The following table shows fast mode pricing for the supported models: | Model | Input | Output | | ------------------------------- | -------------- | -------------- | | Claude Opus 5 / Claude Opus 4.8 | $10 USD / MTok | $50 USD / MTok | Fast mode pricing stacks with other pricing modifiers: * [Prompt caching multipliers](https://platform.claude.com/docs/en/about-claude/pricing#prompt-caching) apply on top of fast mode pricing * [Data residency](https://platform.claude.com/docs/en/manage-claude/data-residency) multipliers apply on top of fast mode pricing For complete pricing details, see the [Pricing](https://platform.claude.com/docs/en/about-claude/pricing#fast-mode-pricing) page. ## Rate limits Fast mode has a dedicated rate limit that is separate from standard Opus rate limits. When your fast mode rate limit is exceeded, the API returns a `429` error with a `retry-after` header indicating when capacity will be available. The response includes headers that indicate your fast mode rate limit status: | Header | Description | | ---------------------------------------- | ------------------------------------------------- | | `anthropic-fast-input-tokens-limit` | Maximum fast mode input tokens per minute | | `anthropic-fast-input-tokens-remaining` | Remaining fast mode input tokens | | `anthropic-fast-input-tokens-reset` | Time when the fast mode input token limit resets | | `anthropic-fast-output-tokens-limit` | Maximum fast mode output tokens per minute | | `anthropic-fast-output-tokens-remaining` | Remaining fast mode output tokens | | `anthropic-fast-output-tokens-reset` | Time when the fast mode output token limit resets | For tier-specific rate limits, see the [Rate limits](https://platform.claude.com/docs/en/api/rate-limits) page. ## Checking which speed was used The response `usage` object includes a `speed` field that indicates which speed was used, either `"fast"` or `"standard"`. Requesting `speed: "fast"` on a [model that doesn't support fast mode](https://platform.claude.com/docs/en/build-with-claude/fast-mode#supported-models) returns an error, and so does exceeding fast mode's rate limits or capacity (a `429` or `529`). When a request with `speed: "fast"` succeeds, `usage.speed` is `"fast"`. If you are using Claude Opus 4.6 and request fast mode, its behavior is unique. Instead of returning an error like other models that don't support fast mode, it silently switches to standard speed. Though there is no error with Opus 4.6, the `speed` field accurately shows `"standard"`. ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: fast-mode-2026-02-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "speed": "fast", "messages": [{"role": "user", "content": "Hello"}] }' ``` ```bash CLI ant beta:messages create \ --beta fast-mode-2026-02-01 \ --transform usage.speed \ --raw-output <<'YAML' model: claude-opus-5 max_tokens: 1024 speed: fast messages: - role: user content: Hello YAML ``` ```python Python client = anthropic.Anthropic() response = client.beta.messages.create( model="claude-opus-5", max_tokens=1024, speed="fast", betas=["fast-mode-2026-02-01"], messages=[{"role": "user", "content": "Hello"}], ) print(response.usage.speed) # "fast" or "standard" ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.beta.messages.create({ model: "claude-opus-5", max_tokens: 1024, speed: "fast", betas: ["fast-mode-2026-02-01"], messages: [{ role: "user", content: "Hello" }] }); console.log(response.usage.speed); // "fast" or "standard" ``` ```csharp C# AnthropicClient client = new(); var response = await client.Beta.Messages.Create(new MessageCreateParams { Model = "claude-opus-5", MaxTokens = 1024, Speed = Speed.Fast, Betas = ["fast-mode-2026-02-01"], Messages = [new() { Role = Role.User, Content = "Hello" }], }); Console.WriteLine(response.Usage.Speed); // "fast" or "standard" ``` ```go Go client := anthropic.NewClient() response, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Speed: anthropic.BetaMessageNewParamsSpeedFast, Betas: []anthropic.AnthropicBeta{anthropic.AnthropicBetaFastMode2026_02_01}, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Hello")), }, }) if err != nil { log.Fatal(err) } fmt.Println(response.Usage.Speed) // "fast" or "standard" ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .speed(MessageCreateParams.Speed.FAST) .addBeta(AnthropicBeta.FAST_MODE_2026_02_01) .addUserMessage("Hello") .build(); BetaMessage response = client.beta().messages().create(params); IO.println(response.usage().speed().orElseThrow()); // "fast" or "standard" ``` ```php PHP $client = new Client(); $response = $client->beta->messages->create( model: 'claude-opus-5', maxTokens: 1024, speed: 'fast', betas: ['fast-mode-2026-02-01'], messages: [['role' => 'user', 'content' => 'Hello']], ); echo $response->usage->speed; // "fast" or "standard" ``` ```ruby Ruby client = Anthropic::Client.new response = client.beta.messages.create( model: "claude-opus-5", max_tokens: 1024, speed: "fast", betas: ["fast-mode-2026-02-01"], messages: [{ role: "user", content: "Hello" }] ) puts(response.usage.speed) # "fast" or "standard" ``` ```json Output { "id": "msg_01XFDUDYJgAACzvnptvVoYEL", "type": "message", "role": "assistant", // ... "usage": { "input_tokens": 8, "output_tokens": 12, "speed": "fast" } } ``` To track fast mode usage and costs across your organization, see the [Usage and Cost API](https://platform.claude.com/docs/en/manage-claude/usage-cost-api). ## Retries and fallback ### Automatic retries When fast mode rate limits are exceeded, the API returns a `429` error with a `retry-after` header. The Anthropic SDKs automatically retry these requests up to 2 times by default (configurable with `max_retries`), waiting for the server-specified delay before each retry. Because fast mode uses continuous token replenishment, the `retry-after` delay is typically short and requests succeed once capacity is available. ### Falling back to standard speed This section covers an opt-in client-side fallback when fast mode is rate limited. It is separate from the behavior on [Claude Opus 4.6](https://platform.claude.com/docs/en/build-with-claude/fast-mode#supported-models), where fast mode is not available and requests run at standard speed automatically. If you'd prefer to fall back to standard speed rather than wait for fast mode capacity, catch the rate limit error and retry without `speed: "fast"`. Set `max_retries` to `0` on the initial fast request to skip automatic retries and fail immediately on rate limit errors. Falling back from fast to standard speed will result in a [prompt cache](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) miss. Requests at different speeds do not share cached prefixes. Because setting `max_retries` to `0` also disables retries for other transient errors (overloaded, internal server errors), the following examples reissue the original request with default retries for those cases. ```bash CLI # `ant` retries 429/5xx automatically and has no per-request max_retries # override, so on a fast-mode 429 the fallback runs after the built-in # retries exhaust. --transform-error surfaces error.type for branching. create_message_with_fast_fallback() { local speed="$1" max_attempts="${2:-3}" body out body=${3:-$(cat)} out=$( ant beta:messages create --beta fast-mode-2026-02-01 \ ${speed:+--speed "$speed"} \ --transform-error error.type --format-error yaml <<<"$body" 2>/dev/null ) && { printf '%s\n' "$out"; return; } case "$out" in rate_limit_error) if [[ -n "$speed" ]]; then create_message_with_fast_fallback "" "$max_attempts" "$body" return fi ;; overloaded_error | api_error | "") if (( max_attempts > 1 )); then create_message_with_fast_fallback "$speed" $((max_attempts - 1)) "$body" return fi ;; esac printf '%s\n' "${out:-connection_error}" >&2 return 1 } MESSAGE=$( create_message_with_fast_fallback fast <<'YAML' model: claude-opus-5 max_tokens: 1024 messages: - role: user content: Hello YAML ) ``` ```python Python client = anthropic.Anthropic() def create_message_with_fast_fallback(max_retries=0, max_attempts=3, **params): try: return client.with_options(max_retries=max_retries).beta.messages.create( **params ) except anthropic.RateLimitError: if params.get("speed") == "fast": del params["speed"] return create_message_with_fast_fallback(max_retries=max_retries, **params) raise except ( anthropic.APIStatusError, anthropic.APIConnectionError, ) as error: if isinstance(error, anthropic.APIStatusError) and error.status_code < 500: raise if max_attempts > 1: return create_message_with_fast_fallback( max_retries=max_retries, max_attempts=max_attempts - 1, **params ) raise message = create_message_with_fast_fallback( model="claude-opus-5", max_tokens=1024, messages=[{"role": "user", "content": "Hello"}], betas=["fast-mode-2026-02-01"], speed="fast", max_retries=0, ) ``` ```typescript TypeScript const client = new Anthropic(); async function createMessageWithFastFallback( params: Anthropic.Beta.MessageCreateParamsNonStreaming, requestOptions?: Anthropic.RequestOptions, maxAttempts: number = 3 ): Promise { try { return await client.beta.messages.create(params, requestOptions); } catch (e) { if (e instanceof Anthropic.RateLimitError && params.speed === "fast") { const { speed, ...rest } = params; return createMessageWithFastFallback(rest); } if ( e instanceof Anthropic.InternalServerError || e instanceof Anthropic.APIConnectionError ) { if (maxAttempts > 1) { return createMessageWithFastFallback(params, undefined, maxAttempts - 1); } } throw e; } } const message = await createMessageWithFastFallback( { model: "claude-opus-5", max_tokens: 1024, messages: [{ role: "user", content: "Hello" }], betas: ["fast-mode-2026-02-01"], speed: "fast" }, { maxRetries: 0 } ); ``` ```csharp C# AnthropicClient client = new(); async Task CreateMessageWithFastFallback( MessageCreateParams parameters, int? maxRetries = null, int maxAttempts = 3) { try { var requestClient = maxRetries is int retries ? client.WithOptions(options => options with { MaxRetries = retries }) : client; return await requestClient.Beta.Messages.Create(parameters); } catch (AnthropicRateLimitException) { if (parameters.Speed is not null) { return await CreateMessageWithFastFallback( parameters with { Speed = null }); } throw; } catch (Anthropic5xxException) { if (maxAttempts > 1) { return await CreateMessageWithFastFallback( parameters, maxAttempts: maxAttempts - 1); } throw; } } var message = await CreateMessageWithFastFallback( new MessageCreateParams { Model = "claude-opus-5", MaxTokens = 1024, Messages = [new() { Role = Role.User, Content = "Hello" }], Betas = ["fast-mode-2026-02-01"], Speed = Speed.Fast, }, maxRetries: 0); ``` ```go Go func createMessageWithFastFallback( ctx context.Context, client *anthropic.Client, params anthropic.BetaMessageNewParams, maxAttempts int, opts ...option.RequestOption, ) (*anthropic.BetaMessage, error) { message, err := client.Beta.Messages.New(ctx, params, opts...) if err != nil { var apierr *anthropic.Error if errors.As(err, &apierr) && apierr.StatusCode == 429 && params.Speed != "" { params.Speed = "" return createMessageWithFastFallback(ctx, client, params, maxAttempts) } if (errors.As(err, &apierr) && apierr.StatusCode >= 500) || !errors.As(err, &apierr) { if maxAttempts > 1 { return createMessageWithFastFallback(ctx, client, params, maxAttempts-1) } } return nil, err } return message, nil } func main() { client := anthropic.NewClient() message, err := createMessageWithFastFallback( context.TODO(), &client, anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Hello")), }, Speed: anthropic.BetaMessageNewParamsSpeedFast, Betas: []anthropic.AnthropicBeta{anthropic.AnthropicBetaFastMode2026_02_01}, }, 3, option.WithMaxRetries(0), ) if err != nil { panic(err) } fmt.Println(message) } ``` ```java Java import com.anthropic.errors.InternalServerException; import com.anthropic.errors.RateLimitException; // ... // Disable SDK auto-retry so the fallback logic below handles it AnthropicClient client = AnthropicOkHttpClient.builder().fromEnv().maxRetries(0).build(); BetaMessage createMessageWithFastFallback( MessageCreateParams params, int maxAttempts) { try { return client.beta().messages().create(params); } catch (RateLimitException e) { if (params.speed().isPresent()) { MessageCreateParams retryParams = params.toBuilder() .speed(Optional.empty()) .build(); return createMessageWithFastFallback(retryParams, maxAttempts); } throw e; } catch (InternalServerException e) { if (maxAttempts > 1) { return createMessageWithFastFallback(params, maxAttempts - 1); } throw e; } } void main() { BetaMessage message = createMessageWithFastFallback( MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addUserMessage("Hello") .addBeta(AnthropicBeta.FAST_MODE_2026_02_01) .speed(MessageCreateParams.Speed.FAST) .build(), 3); message.content().stream() .flatMap(block -> block.text().stream()) .forEach(textBlock -> IO.println(textBlock.text())); } ``` ```php PHP use Anthropic\Core\Exceptions\APIConnectionException; use Anthropic\Core\Exceptions\InternalServerException; use Anthropic\Core\Exceptions\RateLimitException; use Anthropic\RequestOptions; // ... $client = new Client(); function createMessageWithFastFallback( Client $client, array $params, ?RequestOptions $requestOptions = null, int $maxAttempts = 3, ) { try { return $client->beta->messages->create( ...$params, requestOptions: $requestOptions, ); } catch (RateLimitException $e) { if (isset($params['speed'])) { unset($params['speed']); return createMessageWithFastFallback($client, $params); } throw $e; } catch (InternalServerException | APIConnectionException $e) { if ($maxAttempts > 1) { return createMessageWithFastFallback( $client, $params, maxAttempts: $maxAttempts - 1 ); } throw $e; } } $message = createMessageWithFastFallback( $client, [ 'model' => 'claude-opus-5', 'maxTokens' => 1024, 'messages' => [['role' => 'user', 'content' => 'Hello']], 'betas' => ['fast-mode-2026-02-01'], 'speed' => 'fast', ], RequestOptions::with(maxRetries: 0), ); ``` ```ruby Ruby client = Anthropic::Client.new def create_message_with_fast_fallback(client, request_options: {}, max_attempts: 3, **params) client.beta.messages.create(**params, request_options: request_options) rescue Anthropic::Errors::RateLimitError raise unless params[:speed] == "fast" params.delete(:speed) create_message_with_fast_fallback(client, **params) rescue Anthropic::Errors::InternalServerError, Anthropic::Errors::APIConnectionError raise unless max_attempts > 1 create_message_with_fast_fallback(client, max_attempts: max_attempts - 1, **params) end message = create_message_with_fast_fallback( client, model: "claude-opus-5", max_tokens: 1024, messages: [{ role: "user", content: "Hello" }], betas: ["fast-mode-2026-02-01"], speed: "fast", request_options: { max_retries: 0 } ) ``` ## Considerations * **Prompt caching:** Switching between fast and standard speed invalidates the prompt cache. Requests at different speeds do not share cached prefixes. * **Supported models:** Fast mode is supported on Claude Opus 5 and Claude Opus 4.8. See [Supported models](https://platform.claude.com/docs/en/build-with-claude/fast-mode#supported-models). * **TTFT:** Fast mode's benefits are focused on output tokens per second (OTPS), not time to first token (TTFT). * **Batch API:** Fast mode is not available with the [Batch API](https://platform.claude.com/docs/en/build-with-claude/batch-processing). * **Priority Tier:** Fast mode is not available with a [Priority Tier](https://platform.claude.com/docs/en/api/service-tiers) commitment. * **Claude Platform on AWS:** Fast mode is not currently available on [Claude Platform on AWS](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws). ## Next steps Get validated JSON results from agent workflows. Learn about Anthropic's pricing structure for models and features. Control how many tokens Claude uses when responding with the effort parameter, trading off between response thoroughness and token efficiency. Stream Messages API responses incrementally with server-sent events, including text, tool use, and extended thinking deltas. --- title: Handle streaming refusals url: https://platform.claude.com/docs/en/test-and-evaluate/strengthen-guardrails/handle-streaming-refusals description: Detect and handle refusal stop reasons in streaming responses, and retry refused requests on a fallback model. --- Starting with Claude 4 models, streaming responses from Claude's API return **`stop_reason`: `"refusal"`** when streaming classifiers intervene to handle potential policy violations. This safety feature helps maintain content compliance during real-time streaming. This page covers how refusals appear in streaming responses. For every `stop_reason` value and how to handle it, see [Stop reasons and fallback](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons). To retry refused requests on another Claude model, see [Refusals and fallback](https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback). ## API response format When streaming classifiers detect content that violates Anthropic's policies, the API returns this response: ```json { "role": "assistant", "content": [ { "type": "text", "text": "Hello.." } ], "stop_reason": "refusal", "stop_details": { "type": "refusal", "category": "cyber", "explanation": "This request was declined because it could enable cyber harm." } } ``` In the event stream, `stop_details` arrives on the `message_delta` event alongside `stop_reason`. A `refusal` response from streaming classifiers includes a `stop_details` object with a `category` and a human-readable `explanation` that you can surface to the user. See [Refusals and fallback](https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback#refusal-response) for the full response shape and the available categories. On a refusal the `stop_details` object is always present, but its `category` and `explanation` fields can be `null`, for example when the refusal maps to no named category. Branch on `stop_reason` or `stop_details.type` rather than assuming `category` and `explanation` are populated, and provide your own user-facing messaging when they are `null`. ## Reset context after refusal When you receive **`stop_reason`: `refusal`**, you must reset the conversation context before continuing. You can remove or rephrase the turn that triggered the refusal, or clear the conversation history entirely. Attempting to continue without resetting will result in continued refusals. Usage metrics are still provided in the response, even when the response is refused. When a refusal arrives before Claude generates any output, you are not billed for the request on the Claude API, and the usage counts in that response are informational only. When Claude generates output before the refusal, you are billed for that request. Resetting context is not the only way to recover. You can also retry the refused request on a different Claude model, and the [Refusals and fallback](https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback) page shows how to set that up with server-side fallback, the SDK middleware, or a manual retry. ## Implementation guide Here's how to detect and handle streaming refusals in your application: ```bash cURL # Stream request and check for refusal response=$(curl -N https://api.anthropic.com/v1/messages \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -d '{ "model": "claude-opus-5", "messages": [{"role": "user", "content": "Hello"}], "max_tokens": 1024, "stream": true }') # Check for refusal in the stream if echo "$response" | grep -q '"stop_reason":"refusal"'; then echo "Response refused - resetting conversation context" # Reset your conversation state here fi ``` ```python Python client = anthropic.Anthropic() messages = [] def reset_conversation(): """Reset conversation context after refusal""" global messages messages = [] print("Conversation reset due to refusal") try: with client.messages.stream( max_tokens=1024, messages=messages + [{"role": "user", "content": "Hello"}], model="claude-opus-5", ) as stream: for event in stream: # Check for refusal in message delta if event.type == "message_delta": if event.delta.stop_reason == "refusal": reset_conversation() break except Exception as e: print(f"Error: {e}") ``` ```typescript TypeScript const client = new Anthropic(); let messages: Anthropic.MessageParam[] = []; function resetConversation() { // Reset conversation context after refusal messages = []; console.log("Conversation reset due to refusal"); } try { const stream = await client.messages.stream({ messages: [...messages, { role: "user", content: "Hello" }], model: "claude-opus-5", max_tokens: 1024 }); for await (const event of stream) { // Check for refusal in message delta if (event.type === "message_delta" && event.delta.stop_reason === "refusal") { resetConversation(); break; } } } catch (error) { console.error("Error:", error); } ``` ```csharp C# List messages = new(); AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [new() { Role = Role.User, Content = "Hello" }] }; try { await foreach (var streamEvent in client.Messages.CreateStreaming(parameters)) { if ( streamEvent.TryPickDelta(out var deltaEvent) && deltaEvent.Delta.StopReason == StopReason.Refusal ) { ResetConversation(); break; } } } catch (Exception e) { Console.WriteLine($"Error: {e.Message}"); } void ResetConversation() { messages.Clear(); Console.WriteLine("Conversation reset due to refusal"); } ``` ```go Go var messages []anthropic.MessageParam func resetConversation() { messages = []anthropic.MessageParam{} fmt.Println("Conversation reset due to refusal") } // ... client := anthropic.NewClient() stream := client.Messages.NewStreaming(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Hello")), }, }) streamLoop: for stream.Next() { event := stream.Current() switch eventVariant := event.AsAny().(type) { case anthropic.MessageDeltaEvent: if eventVariant.Delta.StopReason == anthropic.StopReasonRefusal { resetConversation() break streamLoop } } } if err := stream.Err(); err != nil { log.Fatal(err) } ``` ```java Java import com.anthropic.core.http.StreamResponse; import com.anthropic.models.messages.RawMessageStreamEvent; import com.anthropic.models.messages.StopReason; // ... List messages = new ArrayList<>(); void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addUserMessage("Hello") .build(); try (StreamResponse stream = client.messages().createStreaming(params)) { stream.stream().forEach(event -> { event.messageDelta().ifPresent(deltaEvent -> { deltaEvent.delta().stopReason().ifPresent(stopReason -> { if (stopReason.equals(StopReason.REFUSAL)) { resetConversation(); } }); }); }); } catch (Exception e) { System.err.println("Error: " + e.getMessage()); } } void resetConversation() { messages.clear(); IO.println("Conversation reset due to refusal"); } ``` ```php PHP $client = new Client(); $messages = []; function resetConversation(&$messages) { $messages = []; echo "Conversation reset due to refusal\n"; } try { $stream = $client->messages->createStream( maxTokens: 1024, messages: [ ['role' => 'user', 'content' => 'Hello'] ], model: 'claude-opus-5', ); foreach ($stream as $event) { if ($event->type === 'message_delta' && $event->delta->stopReason === 'refusal') { resetConversation($messages); break; } } } catch (Exception $e) { echo "Error: " . $e->getMessage() . "\n"; } ``` ```ruby Ruby client = Anthropic::Client.new messages = [] def reset_conversation(messages) messages.clear puts "Conversation reset due to refusal" end begin stream = client.messages.stream( model: :"claude-opus-5", max_tokens: 1024, messages: [{ role: "user", content: "Hello" }] ) stream.each do |event| if event.type == :message_delta && event.delta.stop_reason == :refusal reset_conversation(messages) break end end rescue => e puts "Error: #{e.message}" end ``` ## Current refusal types The API currently handles refusals in three different ways: | Refusal type | Response format | When it occurs | | ---------------------------------- | ---------------------------- | ----------------------------------------------- | | Streaming classifier refusals | **`stop_reason`: `refusal`** | During streaming when content violates policies | | API input and copyright validation | 400 error codes | When input fails validation checks | | Model-generated refusals | Standard text responses | When the model itself refuses | ## Best practices * **Monitor for refusals:** Include **`stop_reason`: `refusal`** checks in your error handling * **Reset automatically:** Implement automatic context reset when refusals are detected * **Fall back to another model:** Configure [server-side fallback or the SDK middleware](https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback) so refused requests are retried on another Claude model instead of surfacing a refusal to the user * **Redeem fallback credit on manual retries:** If you build the retry yourself, pass the refusal's [fallback credit](https://platform.claude.com/docs/en/build-with-claude/fallback-credit) token so the retry doesn't pay the prompt-cache cost twice * **Provide custom messaging:** Create user-friendly messages for better UX when refusals occur * **Track refusal patterns:** Monitor refusal frequency to identify potential issues with your prompts ## Migration notes If you built refusal handling when this feature first shipped, or you're adding it to an existing integration, check the following: * **Refusals are responses, not errors.** A refusal arrives as a successful HTTP 200 response with `stop_reason`: `"refusal"`, so monitoring built only on error rates won't surface it. Track refusals as their own signal. * **Refusals include structured detail.** On every model, a refusal also includes a `stop_details` object that identifies the policy category behind the decline. See [Refusals and fallback](https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback#refusal-response) for the full response shape. * **Retry on a different model.** Re-sending a refused request to the same model usually results in another refusal. Instead of only resetting context, retry on a fallback model with [server-side fallback, the SDK middleware, or a manual retry](https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback), and redeem [fallback credit](https://platform.claude.com/docs/en/build-with-claude/fallback-credit) when you build the retry yourself. * **Check batch results for refusals.** A refused request in a [Message Batch](https://platform.claude.com/docs/en/build-with-claude/batch-processing) is returned as a succeeded result with `stop_reason`: `"refusal"`, not as an errored result. * **Centralize handling on `stop_reason`.** The API continues to consolidate refusal handling around `stop_reason`: `"refusal"`, so branch on the stop reason rather than on model-specific behavior. ## Next steps Retry refused requests on another Claude model, server-side or in your client. Every `stop_reason` value and how to handle it. Stream responses and read `stop_reason` from `message_delta` events as they arrive. Serve users across languages with Claude's cross-lingual capabilities. --- title: Multilingual support url: https://platform.claude.com/docs/en/build-with-claude/multilingual-support description: Claude excels at tasks across multiple languages, maintaining strong cross-lingual performance relative to English. --- ## Overview Claude demonstrates robust multilingual capabilities, with particularly strong performance in zero-shot tasks across languages. The model maintains consistent relative performance across both widely spoken and lower-resource languages, making it a reliable choice for multilingual applications. Claude is capable in many languages beyond those benchmarked in the following table. Test with any languages relevant to your specific use cases. ## Performance data The following table shows zero-shot chain-of-thought evaluation scores for Claude models across languages, expressed as a percentage relative to English performance (100%): | Language | Claude Sonnet 4.51 | Claude Haiku 4.51 | | --------------------------------- | ------------------ | ----------------- | | English (baseline, fixed to 100%) | 100% | 100% | | Spanish | 98.2% | 96.4% | | Portuguese (Brazil) | 97.8% | 96.1% | | Italian | 97.9% | 96.0% | | French | 97.5% | 95.7% | | Indonesian | 97.3% | 94.2% | | German | 97.0% | 94.3% | | Arabic | 97.2% | 92.5% | | Chinese (Simplified) | 96.9% | 94.2% | | Korean | 96.7% | 93.3% | | Japanese | 96.8% | 93.5% | | Hindi | 96.7% | 92.4% | | Bengali | 95.4% | 90.4% | | Swahili | 91.1% | 78.3% | | Yoruba | 79.7% | 52.7% | 1 With [extended thinking](https://platform.claude.com/docs/en/build-with-claude/extended-thinking). These metrics are based on [MMLU (Massive Multitask Language Understanding)](https://en.wikipedia.org/wiki/MMLU) English test sets that were translated into 14 additional languages by professional human translators, as documented in [OpenAI's simple-evals repository](https://github.com/openai/simple-evals/blob/main/multilingual_mmlu_benchmark_results.md). The use of human translators for this evaluation ensures high-quality translations, particularly important for languages with fewer digital resources. *** ## Set the response language Claude infers the response language from the conversation, but for production applications you should state the target language explicitly. The most reliable place to do this is the system prompt, which keeps the instruction stable across every turn of a conversation. ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "system": "Always respond in French, regardless of the language the user writes in.", "messages": [ {"role": "user", "content": "How do I reset my password?"} ] }' ``` ```bash CLI ant messages create \ --model claude-opus-5 \ --max-tokens 1024 \ --system "Always respond in French, regardless of the language the user writes in." \ --message '{role: user, content: "How do I reset my password?"}' ``` ```python Python client = anthropic.Anthropic() message = client.messages.create( model="claude-opus-5", max_tokens=1024, system="Always respond in French, regardless of the language the user writes in.", messages=[{"role": "user", "content": "How do I reset my password?"}], ) print(message.content) ``` ```typescript TypeScript const client = new Anthropic(); const message = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, system: "Always respond in French, regardless of the language the user writes in.", messages: [{ role: "user", content: "How do I reset my password?" }] }); console.log(message.content); ``` ```csharp C# AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, System = "Always respond in French, regardless of the language the user writes in.", Messages = [ new() { Role = Role.User, Content = "How do I reset my password?" } ] }; var message = await client.Messages.Create(parameters); Console.WriteLine(message); ``` ```go Go client := anthropic.NewClient() message, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, System: []anthropic.TextBlockParam{ {Text: "Always respond in French, regardless of the language the user writes in."}, }, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("How do I reset my password?")), }, }) if err != nil { log.Fatal(err) } fmt.Println(message.Content) ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .system("Always respond in French, regardless of the language the user writes in.") .addUserMessage("How do I reset my password?") .build(); Message message = client.messages().create(params); System.out.println(message.content()); ``` ```php PHP $client = new Client(); $message = $client->messages->create( maxTokens: 1024, messages: [ ['role' => 'user', 'content' => 'How do I reset my password?'] ], model: 'claude-opus-5', system: 'Always respond in French, regardless of the language the user writes in.', ); echo json_encode($message->content, JSON_PRETTY_PRINT), PHP_EOL; ``` ```ruby Ruby client = Anthropic::Client.new message = client.messages.create( model: "claude-opus-5", max_tokens: 1024, system: "Always respond in French, regardless of the language the user writes in.", messages: [ { role: "user", content: "How do I reset my password?" } ] ) puts message.content ``` If your application lets users pick a language at runtime, interpolate that choice into the system prompt rather than relying on Claude to infer it from the user's message. To translate between two specific languages, name both: `Translate the user's message from German to Korean. Respond with only the translation.` *** ## Best practices When working with multilingual content: 1. **Provide clear language context:** Although Claude can detect the target language automatically, explicitly stating the desired input and output languages improves reliability. For enhanced fluency, you can prompt Claude to use "idiomatic speech as if it were a native speaker." 2. **Use native scripts:** Submit text in its native script rather than transliteration for optimal results. 3. **Consider cultural context:** Effective communication often requires cultural and regional awareness beyond pure translation. Also follow the general guidance in [Prompt engineering overview](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/overview) to further improve output quality. *** ## Language support considerations * Claude processes input and generates output in most world languages that use standard Unicode characters. * Performance varies by language, with particularly strong capabilities in widely spoken languages. * Even in languages with fewer digital resources, Claude maintains meaningful capabilities. ## Next steps Apply general prompting techniques to improve multilingual output quality. Build a localized support chatbot using a language-constrained system prompt. Compare model tiers to balance multilingual quality against cost and latency. Evaluate translation and localization quality before you ship. --- title: Search results url: https://platform.claude.com/docs/en/build-with-claude/search-results description: Enable natural citations for RAG applications by providing search results with source attribution --- For how zero data retention (ZDR) applies to this feature, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention). Search result content blocks let Claude cite your own content the same way it cites web search results: each citation carries the source and title you provided. Use them in RAG (Retrieval-Augmented Generation) applications where Claude needs to attribute answers to your documents. All [active models](https://platform.claude.com/docs/en/about-claude/models/overview) support search results with citations, with the exception of Claude Haiku 3. No beta header is required: search results are part of the standard Messages API. ## How it works Search results can be provided in two ways: 1. **From tool calls:** Your custom tools return search results, enabling dynamic RAG applications 2. **As top-level content:** You provide search results directly in user messages for pre-fetched or cached content In both cases, Claude cites the search results automatically when citations are enabled. No special prompting is needed: ask your question, and citations appear on the text blocks that draw on your content. ### Search result schema Search results use the following structure: ```json { "type": "search_result", "source": "https://example.com/article", // Required: Source URL or identifier "title": "Article Title", // Required: Title of the result "content": [ // Required: Array of text blocks { "type": "text", "text": "The actual content of the search result..." } ], "citations": { // Optional: Citation configuration "enabled": true // Enable/disable citations for this result } } ``` ### Required fields | Field | Type | Description | | --------- | ------ | ---------------------------------------------------------------------------------------------------------------- | | `type` | string | Must be `"search_result"` | | `source` | string | The source of the content. Any stable string works: a URL, or an internal identifier such as `kb://article-1234` | | `title` | string | A descriptive title for the search result | | `content` | array | An array of text blocks containing the actual content | ### Optional fields | Field | Type | Description | | --------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `citations` | object | Citation configuration with `enabled` Boolean field. Citations are disabled by default; every example on this page sets `"enabled": true` explicitly. All search results in a request must use the same setting (see [Citation control](https://platform.claude.com/docs/en/build-with-claude/search-results#citation-control)) | | `cache_control` | object | Cache control settings (for example, `{"type": "ephemeral"}`) | Each item in the `content` array must be a text block with: * `type`: Must be `"text"` * `text`: The actual text content (non-empty string) Search results hold text only. Images and other media are not supported inside the `content` array. ## Method 1: Search results from tool calls Returning search results from your custom tools enables dynamic RAG applications: tools fetch content at runtime, and Claude cites it in the response. The following example forces the tool call with [`tool_choice`](https://platform.claude.com/docs/en/agents-and-tools/tool-use/define-tools#forcing-tool-use), so the retrieval step runs every time. ### Example: Knowledge base tool ```bash cURL # The tool-calling flow needs application-side search logic that doesn't # translate to a one-off shell command. See the SDK tabs for the full flow. # The raw shape of a tool conversation with search results is shown in the # Combining both methods cURL tab; Method 2 shows the top-level shape. ``` ```bash CLI # The tool-calling flow needs application-side search logic that doesn't # translate to a one-off shell command. See the SDK tabs for the full flow. # The raw shape of a tool conversation with search results is shown in the # Combining both methods cURL tab; Method 2 shows the top-level shape. ``` ```python Python from anthropic.types import ( MessageParam, TextBlockParam, SearchResultBlockParam, ToolResultBlockParam, ) client = Anthropic() # Define a knowledge base search tool knowledge_base_tool = { "name": "search_knowledge_base", "description": "Search the company knowledge base for information", "input_schema": { "type": "object", "properties": {"query": {"type": "string", "description": "The search query"}}, "required": ["query"], }, } # Function to handle the tool call def search_knowledge_base(query): # Your search logic here # Returns search results in the correct format return [ SearchResultBlockParam( type="search_result", source="https://docs.company.com/product-guide", title="Product Configuration Guide", content=[ TextBlockParam( type="text", text="To configure the product, navigate to Settings > Configuration. The default timeout is 30 seconds, but can be adjusted between 10-120 seconds based on your needs.", ) ], citations={"enabled": True}, ), SearchResultBlockParam( type="search_result", source="https://docs.company.com/troubleshooting", title="Troubleshooting Guide", content=[ TextBlockParam( type="text", text="If you encounter timeout errors, first check the configuration settings. Common causes include network latency and incorrect timeout values.", ) ], citations={"enabled": True}, ), ] # Build up the conversation in a list, starting with the user's question messages = [ MessageParam(role="user", content="How do I configure the timeout settings?") ] # Create a message with the tool response = client.messages.create( model="claude-opus-5", max_tokens=1024, tools=[knowledge_base_tool], tool_choice={"type": "tool", "name": "search_knowledge_base"}, messages=messages, ) # When Claude calls the tool, provide the search results. # The tool_use block is not always first: iterate to find it. tool_use = next((block for block in response.content if block.type == "tool_use"), None) if tool_use is not None: tool_result = search_knowledge_base(tool_use.input["query"]) # Append Claude's turn, then the tool result, to the running conversation messages.append(MessageParam(role="assistant", content=response.content)) messages.append( MessageParam( role="user", content=[ ToolResultBlockParam( type="tool_result", tool_use_id=tool_use.id, content=tool_result, # Search results go here ) ], ) ) # Send the tool result back final_response = client.messages.create( model="claude-opus-5", max_tokens=1024, messages=messages, ) print(final_response) ``` ```typescript TypeScript const client = new Anthropic(); // Define a knowledge base search tool const knowledgeBaseTool: Anthropic.Tool = { name: "search_knowledge_base", description: "Search the company knowledge base for information", input_schema: { type: "object" as const, properties: { query: { type: "string", description: "The search query" } }, required: ["query"] } }; // Function to handle the tool call function searchKnowledgeBase(query: string) { // Your search logic here // Returns search results in the correct format return [ { type: "search_result" as const, source: "https://docs.company.com/product-guide", title: "Product Configuration Guide", content: [ { type: "text" as const, text: "To configure the product, navigate to Settings > Configuration. The default timeout is 30 seconds, but can be adjusted between 10-120 seconds based on your needs." } ], citations: { enabled: true } }, { type: "search_result" as const, source: "https://docs.company.com/troubleshooting", title: "Troubleshooting Guide", content: [ { type: "text" as const, text: "If you encounter timeout errors, first check the configuration settings. Common causes include network latency and incorrect timeout values." } ], citations: { enabled: true } } ]; } // Build up the conversation in a list, starting with the user's question const messages: Anthropic.MessageParam[] = [ { role: "user", content: "How do I configure the timeout settings?" } ]; // Create a message with the tool const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, tools: [knowledgeBaseTool], tool_choice: { type: "tool", name: "search_knowledge_base" }, messages }); // Handle tool use and provide results. // The tool_use block is not always first: find it in the content array. const toolUse = response.content.find( (block): block is Anthropic.ToolUseBlock => block.type === "tool_use" ); if (toolUse) { const input = toolUse.input as { query: string }; const toolResult = searchKnowledgeBase(input.query); // Append Claude's turn, then the tool result, to the running conversation messages.push({ role: "assistant", content: response.content }); messages.push({ role: "user", content: [ { type: "tool_result" as const, tool_use_id: toolUse.id, content: toolResult // Search results go here } ] }); // Send the tool result back const finalResponse = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, messages }); console.log(finalResponse); } ``` ```csharp C# AnthropicClient client = new(); var tools = new List { new ToolUnion(new Tool() { Name = "search_knowledge_base", Description = "Search the company knowledge base for information", InputSchema = new InputSchema() { Properties = new Dictionary { ["query"] = JsonSerializer.SerializeToElement(new { type = "string", description = "The search query" }), }, Required = ["query"], }, }), }; // Function to handle the tool call static List SearchKnowledgeBase(string query) { // Your search logic here // Returns search results in the correct format return [ new SearchResultBlockParam { Source = "https://docs.company.com/product-guide", Title = "Product Configuration Guide", Content = [new() { Text = "To configure the product, navigate to Settings > Configuration. The default timeout is 30 seconds, but can be adjusted between 10-120 seconds based on your needs." }], Citations = new() { Enabled = true }, }, new SearchResultBlockParam { Source = "https://docs.company.com/troubleshooting", Title = "Troubleshooting Guide", Content = [new() { Text = "If you encounter timeout errors, first check the configuration settings. Common causes include network latency and incorrect timeout values." }], Citations = new() { Enabled = true }, }, ]; } // Build up the conversation in a list, starting with the user's question List messages = [new() { Role = Role.User, Content = "How do I configure the timeout settings?" }]; // Create a message with the tool var response = await client.Messages.Create(new() { Model = Model.ClaudeOpus5, MaxTokens = 1024, Tools = tools, ToolChoice = new ToolChoiceTool { Name = "search_knowledge_base" }, Messages = messages, }); // When Claude calls the tool, provide the search results. // The tool_use block is not always first: find the first one. foreach (var block in response.Content) { if (block.TryPickToolUse(out var toolUse)) { var query = toolUse.Input["query"].GetString() ?? ""; var toolResults = SearchKnowledgeBase(query); // Append Claude's turn, then the tool result, to the running conversation messages.Add(new() { Role = Role.Assistant, Content = response.Content.Select(contentBlock => new ContentBlockParam(contentBlock.Json)).ToList() }); messages.Add(new() { Role = Role.User, Content = new MessageParamContent( [new ContentBlockParam(new ToolResultBlockParam() { ToolUseID = toolUse.ID, Content = new ToolResultBlockParamContent(toolResults) })] ), }); // Send the tool result back var finalResponse = await client.Messages.Create(new() { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = messages, }); Console.WriteLine(finalResponse); break; } } ``` ```go Go client := anthropic.NewClient() knowledgeBaseTool := anthropic.ToolUnionParam{ OfTool: &anthropic.ToolParam{ Name: "search_knowledge_base", Description: anthropic.String("Search the company knowledge base for information"), InputSchema: anthropic.ToolInputSchemaParam{ Properties: map[string]any{ "query": map[string]any{ "type": "string", "description": "The search query", }, }, Required: []string{"query"}, }, }, } // Build up the conversation in a slice, starting with the user's question messages := []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("How do I configure the timeout settings?")), } response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Tools: []anthropic.ToolUnionParam{knowledgeBaseTool}, ToolChoice: anthropic.ToolChoiceUnionParam{OfTool: &anthropic.ToolChoiceToolParam{Name: "search_knowledge_base"}}, Messages: messages, }) if err != nil { log.Fatal(err) } // The tool_use block is not always first: find it in the content list var toolUse *anthropic.ToolUseBlock for _, block := range response.Content { if variant, ok := block.AsAny().(anthropic.ToolUseBlock); ok { toolUse = &variant break } } if toolUse != nil { var input struct { Query string `json:"query"` } if err := json.Unmarshal(toolUse.Input, &input); err != nil { log.Fatal(err) } toolResults := searchKnowledgeBase(input.Query) // Append Claude's turn, then the tool result, to the running conversation messages = append(messages, response.ToParam()) messages = append(messages, anthropic.NewUserMessage(anthropic.ContentBlockParamUnion{ OfToolResult: &anthropic.ToolResultBlockParam{ ToolUseID: toolUse.ID, Content: toolResults, }, })) // Send the tool result back finalResponse, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: messages, }) if err != nil { log.Fatal(err) } fmt.Println(finalResponse) } // ... func searchKnowledgeBase(query string) []anthropic.ToolResultBlockParamContentUnion { return []anthropic.ToolResultBlockParamContentUnion{ {OfSearchResult: &anthropic.SearchResultBlockParam{ Content: []anthropic.TextBlockParam{ {Text: "To configure the product, navigate to Settings > Configuration. The default timeout is 30 seconds, but can be adjusted between 10-120 seconds based on your needs."}, }, Source: "https://docs.company.com/product-guide", Title: "Product Configuration Guide", Citations: anthropic.CitationsConfigParam{Enabled: anthropic.Bool(true)}, }}, {OfSearchResult: &anthropic.SearchResultBlockParam{ Content: []anthropic.TextBlockParam{ {Text: "If you encounter timeout errors, first check the configuration settings. Common causes include network latency and incorrect timeout values."}, }, Source: "https://docs.company.com/troubleshooting", Title: "Troubleshooting Guide", Citations: anthropic.CitationsConfigParam{Enabled: anthropic.Bool(true)}, }}, } } ``` ```java Java import com.anthropic.models.messages.ContentBlockParam; import com.anthropic.models.messages.CitationsConfigParam; // ... import com.anthropic.models.messages.MessageParam; import com.anthropic.models.messages.Model; import com.anthropic.models.messages.SearchResultBlockParam; import com.anthropic.models.messages.TextBlockParam; import com.anthropic.models.messages.Tool; import com.anthropic.models.messages.ToolChoice; import com.anthropic.models.messages.ToolChoiceTool; import com.anthropic.models.messages.ToolResultBlockParam; import com.anthropic.core.JsonValue; // ... void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); Tool knowledgeBaseTool = Tool.builder() .name("search_knowledge_base") .description("Search the company knowledge base for information") .inputSchema(Tool.InputSchema.builder() .properties(JsonValue.from(Map.of( "query", Map.of( "type", "string", "description", "The search query" ) ))) .putAdditionalProperty("required", JsonValue.from(List.of("query"))) .build()) .build(); // Build up the conversation in a list, starting with the user's question List messages = new ArrayList<>(); messages.add(MessageParam.builder() .role(MessageParam.Role.USER) .content("How do I configure the timeout settings?") .build()); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addTool(knowledgeBaseTool) .toolChoice(ToolChoice.ofTool(ToolChoiceTool.builder() .name("search_knowledge_base") .build())) .messages(messages) .build(); Message response = client.messages().create(params); // The tool_use block is not always first: find it in the content list response.content().stream() .flatMap(contentBlock -> contentBlock.toolUse().stream()) .findFirst() .ifPresent(toolUse -> { Map input = (Map) toolUse._input().asObject().get(); List toolResult = searchKnowledgeBase( input.get("query").asStringOrThrow() ); // Append Claude's entire turn to the running conversation, then the tool result. // Rebuilding only the tool_use block would drop any other content blocks Claude // returned (e.g. leading text when the tool call is not forced) — append the // full turn, as the other language tabs do. messages.add(MessageParam.builder() .role(MessageParam.Role.ASSISTANT) .contentOfBlockParams( response.content().stream() .map(block -> block.toParam()) .toList() ) .build()); messages.add(MessageParam.builder() .role(MessageParam.Role.USER) .contentOfBlockParams(List.of( ContentBlockParam.ofToolResult( ToolResultBlockParam.builder() .toolUseId(toolUse.id()) .contentOfBlocks(toolResult) .build() ) )) .build()); // Send the tool result back MessageCreateParams finalParams = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .messages(messages) .build(); Message finalResponse = client.messages().create(finalParams); System.out.println(finalResponse); }); } static List searchKnowledgeBase(String query) { return List.of( ToolResultBlockParam.Content.Block.ofSearchResult( SearchResultBlockParam.builder() .source("https://docs.company.com/product-guide") .title("Product Configuration Guide") .content(List.of( TextBlockParam.builder() .text("To configure the product, navigate to Settings > Configuration. The default timeout is 30 seconds, but can be adjusted between 10-120 seconds based on your needs.") .build() )) .citations(CitationsConfigParam.builder().enabled(true).build()) .build() ), ToolResultBlockParam.Content.Block.ofSearchResult( SearchResultBlockParam.builder() .source("https://docs.company.com/troubleshooting") .title("Troubleshooting Guide") .content(List.of( TextBlockParam.builder() .text("If you encounter timeout errors, first check the configuration settings. Common causes include network latency and incorrect timeout values.") .build() )) .citations(CitationsConfigParam.builder().enabled(true).build()) .build() ) ); } ``` ```php PHP $client = new Client(); $knowledgeBaseTool = [ 'name' => 'search_knowledge_base', 'description' => 'Search the company knowledge base for information', 'input_schema' => [ 'type' => 'object', 'properties' => [ 'query' => [ 'type' => 'string', 'description' => 'The search query' ] ], 'required' => ['query'] ] ]; function searchKnowledgeBase($query) { return [ [ 'type' => 'search_result', 'source' => 'https://docs.company.com/product-guide', 'title' => 'Product Configuration Guide', 'content' => [ [ 'type' => 'text', 'text' => 'To configure the product, navigate to Settings > Configuration. The default timeout is 30 seconds, but can be adjusted between 10-120 seconds based on your needs.' ] ], 'citations' => ['enabled' => true] ], [ 'type' => 'search_result', 'source' => 'https://docs.company.com/troubleshooting', 'title' => 'Troubleshooting Guide', 'content' => [ [ 'type' => 'text', 'text' => 'If you encounter timeout errors, first check the configuration settings. Common causes include network latency and incorrect timeout values.' ] ], 'citations' => ['enabled' => true] ] ]; } // Build up the conversation in a list, starting with the user's question $messages = [ ['role' => 'user', 'content' => 'How do I configure the timeout settings?'] ]; $response = $client->messages->create( maxTokens: 1024, messages: $messages, model: 'claude-opus-5', toolChoice: ['type' => 'tool', 'name' => 'search_knowledge_base'], tools: [$knowledgeBaseTool], ); $toolUseBlock = null; foreach ($response->content as $block) { if ($block->type === 'tool_use') { $toolUseBlock = $block; break; } } if ($toolUseBlock !== null) { $toolResult = searchKnowledgeBase($toolUseBlock->input['query']); // Append Claude's turn, then the tool result, to the running conversation $messages[] = ['role' => 'assistant', 'content' => $response->content]; $messages[] = [ 'role' => 'user', 'content' => [ [ 'type' => 'tool_result', 'tool_use_id' => $toolUseBlock->id, 'content' => $toolResult ] ] ]; // Send the tool result back $finalResponse = $client->messages->create( maxTokens: 1024, messages: $messages, model: 'claude-opus-5', ); echo $finalResponse; } else { echo $response; } ``` ```ruby Ruby client = Anthropic::Client.new knowledge_base_tool = { name: "search_knowledge_base", description: "Search the company knowledge base for information", input_schema: { type: "object", properties: { query: { type: "string", description: "The search query" } }, required: ["query"] } } def search_knowledge_base(query) [ { type: "search_result", source: "https://docs.company.com/product-guide", title: "Product Configuration Guide", content: [ { type: "text", text: "To configure the product, navigate to Settings > Configuration. The default timeout is 30 seconds, but can be adjusted between 10-120 seconds based on your needs." } ], citations: { enabled: true } }, { type: "search_result", source: "https://docs.company.com/troubleshooting", title: "Troubleshooting Guide", content: [ { type: "text", text: "If you encounter timeout errors, first check the configuration settings. Common causes include network latency and incorrect timeout values." } ], citations: { enabled: true } } ] end # Build up the conversation in a list, starting with the user's question messages = [ { role: "user", content: "How do I configure the timeout settings?" } ] response = client.messages.create( model: "claude-opus-5", max_tokens: 1024, tools: [knowledge_base_tool], tool_choice: { type: "tool", name: "search_knowledge_base" }, messages: messages ) # The tool_use block is not always first: find it in the content array tool_use = response.content.find { |block| block.type == :tool_use } if tool_use tool_result = search_knowledge_base(tool_use.input[:query]) # Append Claude's turn, then the tool result, to the running conversation messages << { role: "assistant", content: response.content } messages << { role: "user", content: [ { type: "tool_result", tool_use_id: tool_use.id, content: tool_result } ] } # Send the tool result back final_response = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: messages ) puts final_response end ``` ## Method 2: Search results as top-level content You can also provide search results directly in user messages. This is useful for: * Pre-fetched content from your search infrastructure * Cached search results from previous queries * Content from external search services * Testing and development ### Example: Direct search results ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "messages": [ { "role": "user", "content": [ { "type": "search_result", "source": "https://docs.company.com/api-reference", "title": "API Reference - Authentication", "content": [ { "type": "text", "text": "All API requests must include an API key in the Authorization header. Keys can be generated from the dashboard. Rate limits: 1000 requests per hour for standard tier, 10000 for premium." } ], "citations": { "enabled": true } }, { "type": "search_result", "source": "https://docs.company.com/quickstart", "title": "Getting Started Guide", "content": [ { "type": "text", "text": "To get started: 1) Sign up for an account, 2) Generate an API key from the dashboard, 3) Install our SDK using pip install company-sdk, 4) Initialize the client with your API key." } ], "citations": { "enabled": true } }, { "type": "text", "text": "Based on these search results, how do I authenticate API requests and what are the rate limits?" } ] } ] }' ``` ```bash CLI ant messages create <<'YAML' model: claude-opus-5 max_tokens: 1024 messages: - role: user content: - type: search_result source: https://docs.company.com/api-reference title: API Reference - Authentication content: - type: text text: >- All API requests must include an API key in the Authorization header. Keys can be generated from the dashboard. Rate limits: 1000 requests per hour for standard tier, 10000 for premium. citations: enabled: true - type: search_result source: https://docs.company.com/quickstart title: Getting Started Guide content: - type: text text: >- To get started: 1) Sign up for an account, 2) Generate an API key from the dashboard, 3) Install our SDK using pip install company-sdk, 4) Initialize the client with your API key. citations: enabled: true - type: text text: >- Based on these search results, how do I authenticate API requests and what are the rate limits? YAML ``` ```python Python from anthropic.types import MessageParam, TextBlockParam, SearchResultBlockParam client = Anthropic() # Provide search results directly in the user message response = client.messages.create( model="claude-opus-5", max_tokens=1024, messages=[ MessageParam( role="user", content=[ SearchResultBlockParam( type="search_result", source="https://docs.company.com/api-reference", title="API Reference - Authentication", content=[ TextBlockParam( type="text", text="All API requests must include an API key in the Authorization header. Keys can be generated from the dashboard. Rate limits: 1000 requests per hour for standard tier, 10000 for premium.", ) ], citations={"enabled": True}, ), SearchResultBlockParam( type="search_result", source="https://docs.company.com/quickstart", title="Getting Started Guide", content=[ TextBlockParam( type="text", text="To get started: 1) Sign up for an account, 2) Generate an API key from the dashboard, 3) Install our SDK using pip install company-sdk, 4) Initialize the client with your API key.", ) ], citations={"enabled": True}, ), TextBlockParam( type="text", text="Based on these search results, how do I authenticate API requests and what are the rate limits?", ), ], ) ], ) print(response) ``` ```typescript TypeScript const client = new Anthropic(); // Provide search results directly in the user message const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: [ { type: "search_result" as const, source: "https://docs.company.com/api-reference", title: "API Reference - Authentication", content: [ { type: "text" as const, text: "All API requests must include an API key in the Authorization header. Keys can be generated from the dashboard. Rate limits: 1000 requests per hour for standard tier, 10000 for premium." } ], citations: { enabled: true } }, { type: "search_result" as const, source: "https://docs.company.com/quickstart", title: "Getting Started Guide", content: [ { type: "text" as const, text: "To get started: 1) Sign up for an account, 2) Generate an API key from the dashboard, 3) Install our SDK using pip install company-sdk, 4) Initialize the client with your API key." } ], citations: { enabled: true } }, { type: "text" as const, text: "Based on these search results, how do I authenticate API requests and what are the rate limits?" } ] } ] }); console.log(response); ``` ```csharp C# AnthropicClient client = new(); // Provide search results directly in the user message var response = await client.Messages.Create(new() { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [ new() { Role = Role.User, Content = new MessageParamContent( [ new ContentBlockParam(new SearchResultBlockParam { Source = "https://docs.company.com/api-reference", Title = "API Reference - Authentication", Content = [new() { Text = "All API requests must include an API key in the Authorization header. Keys can be generated from the dashboard. Rate limits: 1000 requests per hour for standard tier, 10000 for premium." }], Citations = new() { Enabled = true }, }), new ContentBlockParam(new SearchResultBlockParam { Source = "https://docs.company.com/quickstart", Title = "Getting Started Guide", Content = [new() { Text = "To get started: 1) Sign up for an account, 2) Generate an API key from the dashboard, 3) Install our SDK using pip install company-sdk, 4) Initialize the client with your API key." }], Citations = new() { Enabled = true }, }), new ContentBlockParam(new TextBlockParam { Text = "Based on these search results, how do I authenticate API requests and what are the rate limits?" }), ]), }, ], }); Console.WriteLine(response); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage( anthropic.ContentBlockParamUnion{OfSearchResult: &anthropic.SearchResultBlockParam{ Content: []anthropic.TextBlockParam{ {Text: "All API requests must include an API key in the Authorization header. Keys can be generated from the dashboard. Rate limits: 1000 requests per hour for standard tier, 10000 for premium."}, }, Source: "https://docs.company.com/api-reference", Title: "API Reference - Authentication", Citations: anthropic.CitationsConfigParam{Enabled: anthropic.Bool(true)}, }}, anthropic.ContentBlockParamUnion{OfSearchResult: &anthropic.SearchResultBlockParam{ Content: []anthropic.TextBlockParam{ {Text: "To get started: 1) Sign up for an account, 2) Generate an API key from the dashboard, 3) Install our SDK using pip install company-sdk, 4) Initialize the client with your API key."}, }, Source: "https://docs.company.com/quickstart", Title: "Getting Started Guide", Citations: anthropic.CitationsConfigParam{Enabled: anthropic.Bool(true)}, }}, anthropic.NewTextBlock("Based on these search results, how do I authenticate API requests and what are the rate limits?"), ), }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java import com.anthropic.models.messages.ContentBlockParam; import com.anthropic.models.messages.CitationsConfigParam; // ... import com.anthropic.models.messages.SearchResultBlockParam; import com.anthropic.models.messages.TextBlockParam; // ... void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addUserMessageOfBlockParams(List.of( ContentBlockParam.ofSearchResult( SearchResultBlockParam.builder() .source("https://docs.company.com/api-reference") .title("API Reference - Authentication") .content(List.of( TextBlockParam.builder() .text("All API requests must include an API key in the Authorization header. Keys can be generated from the dashboard. Rate limits: 1000 requests per hour for standard tier, 10000 for premium.") .build() )) .citations(CitationsConfigParam.builder().enabled(true).build()) .build() ), ContentBlockParam.ofSearchResult( SearchResultBlockParam.builder() .source("https://docs.company.com/quickstart") .title("Getting Started Guide") .content(List.of( TextBlockParam.builder() .text("To get started: 1) Sign up for an account, 2) Generate an API key from the dashboard, 3) Install our SDK using pip install company-sdk, 4) Initialize the client with your API key.") .build() )) .citations(CitationsConfigParam.builder().enabled(true).build()) .build() ), ContentBlockParam.ofText( TextBlockParam.builder() .text("Based on these search results, how do I authenticate API requests and what are the rate limits?") .build() ) )) .build(); Message response = client.messages().create(params); System.out.println(response); } ``` ```php PHP $client = new Client(); $message = $client->messages->create( maxTokens: 1024, messages: [ [ 'role' => 'user', 'content' => [ [ 'type' => 'search_result', 'source' => 'https://docs.company.com/api-reference', 'title' => 'API Reference - Authentication', 'content' => [ [ 'type' => 'text', 'text' => 'All API requests must include an API key in the Authorization header. Keys can be generated from the dashboard. Rate limits: 1000 requests per hour for standard tier, 10000 for premium.' ] ], 'citations' => ['enabled' => true] ], [ 'type' => 'search_result', 'source' => 'https://docs.company.com/quickstart', 'title' => 'Getting Started Guide', 'content' => [ [ 'type' => 'text', 'text' => 'To get started: 1) Sign up for an account, 2) Generate an API key from the dashboard, 3) Install our SDK using pip install company-sdk, 4) Initialize the client with your API key.' ] ], 'citations' => ['enabled' => true] ], [ 'type' => 'text', 'text' => 'Based on these search results, how do I authenticate API requests and what are the rate limits?' ] ] ] ], model: 'claude-opus-5', ); echo json_encode($message, JSON_PRETTY_PRINT); ``` ```ruby Ruby client = Anthropic::Client.new message = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: [ { type: "search_result", source: "https://docs.company.com/api-reference", title: "API Reference - Authentication", content: [ { type: "text", text: "All API requests must include an API key in the Authorization header. Keys can be generated from the dashboard. Rate limits: 1000 requests per hour for standard tier, 10000 for premium." } ], citations: { enabled: true } }, { type: "search_result", source: "https://docs.company.com/quickstart", title: "Getting Started Guide", content: [ { type: "text", text: "To get started: 1) Sign up for an account, 2) Generate an API key from the dashboard, 3) Install our SDK using pip install company-sdk, 4) Initialize the client with your API key." } ], citations: { enabled: true } }, { type: "text", text: "Based on these search results, how do I authenticate API requests and what are the rate limits?" } ] } ] ) puts message ``` ## Claude's response with citations Regardless of how search results are provided, Claude automatically includes citations when using information from them: ```json { "role": "assistant", "content": [ { "type": "text", "text": "All API requests must include an API key in the Authorization header. Keys can be generated from the dashboard.", "citations": [ { "type": "search_result_location", "cited_text": "All API requests must include an API key in the Authorization header. Keys can be generated from the dashboard. Rate limits: 1000 requests per hour for standard tier, 10000 for premium.", "source": "https://docs.company.com/api-reference", "title": "API Reference - Authentication", "search_result_index": 0, "start_block_index": 0, "end_block_index": 1 } ] }, { "type": "text", "text": "\n\nTo set this up from scratch, you'll need to " }, { "type": "text", "text": "sign up for an account, generate an API key from the dashboard, install the SDK using `pip install company-sdk`, and initialize the client with your API key.", "citations": [ { "type": "search_result_location", "cited_text": "To get started: 1) Sign up for an account, 2) Generate an API key from the dashboard, 3) Install our SDK using pip install company-sdk, 4) Initialize the client with your API key.", "source": "https://docs.company.com/quickstart", "title": "Getting Started Guide", "search_result_index": 1, "start_block_index": 0, "end_block_index": 1 } ] } ] } ``` ### Citation fields Each citation includes: | Field | Type | Description | | --------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `type` | string | Always `"search_result_location"` for search result citations | | `source` | string | The source from the original search result | | `title` | string or null | The title from the original search result | | `cited_text` | string | The full text of the cited block(s), concatenated. Equals the contents of `content[start_block_index:end_block_index]` joined together. Not counted toward output tokens. | | `search_result_index` | integer | 0-based index of the cited search result among all `search_result` blocks in the request, in the order they appear (across all messages and tool results). | | `start_block_index` | integer | 0-based index of the first cited block in the search result's `content` array. | | `end_block_index` | integer | Exclusive end index of the cited block range in the search result's `content` array. Always greater than `start_block_index`. | The block indices identify a slice of the search result's `content` array, and `cited_text` is the full text of that slice. The text block is the minimal citable unit: Claude cites whole blocks, not substrings within a block. To get finer-grained citations, split your search result content into smaller blocks (see [Multiple content blocks](https://platform.claude.com/docs/en/build-with-claude/search-results#multiple-content-blocks)). ## Multiple content blocks Search results can contain multiple text blocks in the `content` array: ```json { "type": "search_result", "source": "https://docs.company.com/api-guide", "title": "API Documentation", "content": [ { "type": "text", "text": "Authentication: All API requests require an API key." }, { "type": "text", "text": "Rate Limits: The API allows 1000 requests per hour per key." }, { "type": "text", "text": "Error Handling: The API returns standard HTTP status codes." } ], "citations": { "enabled": true } } ``` A citation referencing the rate limits block looks like: ```json { "type": "search_result_location", "cited_text": "Rate Limits: The API allows 1000 requests per hour per key.", "source": "https://docs.company.com/api-guide", "title": "API Documentation", "search_result_index": 0, "start_block_index": 1, "end_block_index": 2 } ``` When this search result is cited, `start_block_index` and `end_block_index` identify which of these blocks the citation covers, and `cited_text` contains exactly those blocks' text. Splitting content into smaller, focused blocks gives Claude finer citation boundaries; combining content into one block means every citation returns the full text. This is the same model used by [custom content documents](https://platform.claude.com/docs/en/build-with-claude/citations#custom-content-documents) in the Citations feature. ## Advanced usage ### Combining both methods You can mix both methods in the same conversation. Claude cites from either source, and `search_result_index` counts all `search_result` blocks in request order, regardless of source. The following example replays a complete conversation. The first user message carries a pre-fetched search result, the assistant turn calls a knowledge base tool, and the tool result returns a second search result. Claude's answer cites both sources: ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "tools": [ { "name": "search_knowledge_base", "description": "Search the company knowledge base for information", "input_schema": { "type": "object", "properties": { "query": {"type": "string", "description": "The search query"} }, "required": ["query"] } } ], "messages": [ { "role": "user", "content": [ { "type": "search_result", "source": "https://docs.company.com/overview", "title": "Product Overview", "content": [ { "type": "text", "text": "Acme Dashboard is a monitoring tool for distributed systems. It supports real-time alerting and custom metric dashboards." } ], "citations": {"enabled": true} }, { "type": "text", "text": "What does Acme Dashboard do, and what plans is it available on?" } ] }, { "role": "assistant", "content": [ { "type": "text", "text": "Let me check the pricing information." }, { "type": "tool_use", "id": "toolu_01A09q90qw90lq917835lq9", "name": "search_knowledge_base", "input": {"query": "Acme Dashboard pricing plans"} } ] }, { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_01A09q90qw90lq917835lq9", "content": [ { "type": "search_result", "source": "https://docs.company.com/pricing", "title": "Pricing Plans", "content": [ { "type": "text", "text": "Acme Dashboard is available on the Starter plan at $10 per user per month and the Enterprise plan with custom pricing." } ], "citations": {"enabled": true} } ] } ] } ] }' ``` ```bash CLI ant messages create <<'YAML' model: claude-opus-5 max_tokens: 1024 tools: - name: search_knowledge_base description: Search the company knowledge base for information input_schema: type: object properties: query: type: string description: The search query required: [query] messages: - role: user content: - type: search_result source: https://docs.company.com/overview title: Product Overview content: - type: text text: >- Acme Dashboard is a monitoring tool for distributed systems. It supports real-time alerting and custom metric dashboards. citations: enabled: true - type: text text: What does Acme Dashboard do, and what plans is it available on? - role: assistant content: - type: text text: Let me check the pricing information. - type: tool_use id: toolu_01A09q90qw90lq917835lq9 name: search_knowledge_base input: query: Acme Dashboard pricing plans - role: user content: - type: tool_result tool_use_id: toolu_01A09q90qw90lq917835lq9 content: - type: search_result source: https://docs.company.com/pricing title: Pricing Plans content: - type: text text: >- Acme Dashboard is available on the Starter plan at $10 per user per month and the Enterprise plan with custom pricing. citations: enabled: true YAML ``` ```python Python from anthropic.types import ( MessageParam, SearchResultBlockParam, TextBlockParam, ToolResultBlockParam, ToolUseBlockParam, ) client = Anthropic() knowledge_base_tool = { "name": "search_knowledge_base", "description": "Search the company knowledge base for information", "input_schema": { "type": "object", "properties": {"query": {"type": "string", "description": "The search query"}}, "required": ["query"], }, } # Replay a conversation that provides search results both ways: the first # user message carries a pre-fetched result, the tool result returns another response = client.messages.create( model="claude-opus-5", max_tokens=1024, tools=[knowledge_base_tool], messages=[ MessageParam( role="user", content=[ SearchResultBlockParam( type="search_result", source="https://docs.company.com/overview", title="Product Overview", content=[ TextBlockParam( type="text", text="Acme Dashboard is a monitoring tool for distributed systems. It supports real-time alerting and custom metric dashboards.", ) ], citations={"enabled": True}, ), TextBlockParam( type="text", text="What does Acme Dashboard do, and what plans is it available on?", ), ], ), MessageParam( role="assistant", content=[ TextBlockParam( type="text", text="Let me check the pricing information." ), ToolUseBlockParam( type="tool_use", id="toolu_01A09q90qw90lq917835lq9", name="search_knowledge_base", input={"query": "Acme Dashboard pricing plans"}, ), ], ), MessageParam( role="user", content=[ ToolResultBlockParam( type="tool_result", tool_use_id="toolu_01A09q90qw90lq917835lq9", content=[ SearchResultBlockParam( type="search_result", source="https://docs.company.com/pricing", title="Pricing Plans", content=[ TextBlockParam( type="text", text="Acme Dashboard is available on the Starter plan at $10 per user per month and the Enterprise plan with custom pricing.", ) ], citations={"enabled": True}, ) ], ) ], ), ], ) print(response) ``` ```typescript TypeScript const client = new Anthropic(); const knowledgeBaseTool: Anthropic.Tool = { name: "search_knowledge_base", description: "Search the company knowledge base for information", input_schema: { type: "object" as const, properties: { query: { type: "string", description: "The search query" } }, required: ["query"] } }; // Replay a conversation that provides search results both ways: the first // user message carries a pre-fetched result, the tool result returns another const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, tools: [knowledgeBaseTool], messages: [ { role: "user", content: [ { type: "search_result" as const, source: "https://docs.company.com/overview", title: "Product Overview", content: [ { type: "text" as const, text: "Acme Dashboard is a monitoring tool for distributed systems. It supports real-time alerting and custom metric dashboards." } ], citations: { enabled: true } }, { type: "text" as const, text: "What does Acme Dashboard do, and what plans is it available on?" } ] }, { role: "assistant", content: [ { type: "text" as const, text: "Let me check the pricing information." }, { type: "tool_use" as const, id: "toolu_01A09q90qw90lq917835lq9", name: "search_knowledge_base", input: { query: "Acme Dashboard pricing plans" } } ] }, { role: "user", content: [ { type: "tool_result" as const, tool_use_id: "toolu_01A09q90qw90lq917835lq9", content: [ { type: "search_result" as const, source: "https://docs.company.com/pricing", title: "Pricing Plans", content: [ { type: "text" as const, text: "Acme Dashboard is available on the Starter plan at $10 per user per month and the Enterprise plan with custom pricing." } ], citations: { enabled: true } } ] } ] } ] }); console.log(response); ``` ```csharp C# AnthropicClient client = new(); // Replay a conversation that provides search results both ways: the first // user message carries a pre-fetched result, the tool result returns another var response = await client.Messages.Create(new() { Model = Model.ClaudeOpus5, MaxTokens = 1024, Tools = [ new ToolUnion(new Tool() { Name = "search_knowledge_base", Description = "Search the company knowledge base for information", InputSchema = new InputSchema() { Properties = new Dictionary { ["query"] = JsonSerializer.SerializeToElement(new { type = "string", description = "The search query" }), }, Required = ["query"], }, }), ], Messages = [ new() { Role = Role.User, Content = new MessageParamContent( [ new ContentBlockParam(new SearchResultBlockParam { Source = "https://docs.company.com/overview", Title = "Product Overview", Content = [new() { Text = "Acme Dashboard is a monitoring tool for distributed systems. It supports real-time alerting and custom metric dashboards." }], Citations = new() { Enabled = true }, }), new ContentBlockParam(new TextBlockParam { Text = "What does Acme Dashboard do, and what plans is it available on?" }), ]), }, new() { Role = Role.Assistant, Content = new MessageParamContent( [ new ContentBlockParam(new TextBlockParam { Text = "Let me check the pricing information." }), new ContentBlockParam(new ToolUseBlockParam { ID = "toolu_01A09q90qw90lq917835lq9", Name = "search_knowledge_base", Input = new Dictionary { ["query"] = JsonSerializer.SerializeToElement("Acme Dashboard pricing plans"), }, }), ]), }, new() { Role = Role.User, Content = new MessageParamContent( [ new ContentBlockParam(new ToolResultBlockParam() { ToolUseID = "toolu_01A09q90qw90lq917835lq9", Content = new ToolResultBlockParamContent( [ new SearchResultBlockParam { Source = "https://docs.company.com/pricing", Title = "Pricing Plans", Content = [new() { Text = "Acme Dashboard is available on the Starter plan at $10 per user per month and the Enterprise plan with custom pricing." }], Citations = new() { Enabled = true }, }, ]), }), ]), }, ], }); Console.WriteLine(response); ``` ```go Go client := anthropic.NewClient() knowledgeBaseTool := anthropic.ToolUnionParam{ OfTool: &anthropic.ToolParam{ Name: "search_knowledge_base", Description: anthropic.String("Search the company knowledge base for information"), InputSchema: anthropic.ToolInputSchemaParam{ Properties: map[string]any{ "query": map[string]any{"type": "string", "description": "The search query"}, }, Required: []string{"query"}, }, }, } // Replay a conversation that provides search results both ways: the first // user message carries a pre-fetched result, the tool result returns another response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Tools: []anthropic.ToolUnionParam{knowledgeBaseTool}, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage( anthropic.ContentBlockParamUnion{OfSearchResult: &anthropic.SearchResultBlockParam{ Content: []anthropic.TextBlockParam{ {Text: "Acme Dashboard is a monitoring tool for distributed systems. It supports real-time alerting and custom metric dashboards."}, }, Source: "https://docs.company.com/overview", Title: "Product Overview", Citations: anthropic.CitationsConfigParam{Enabled: anthropic.Bool(true)}, }}, anthropic.NewTextBlock("What does Acme Dashboard do, and what plans is it available on?"), ), anthropic.NewAssistantMessage( anthropic.NewTextBlock("Let me check the pricing information."), anthropic.ContentBlockParamUnion{OfToolUse: &anthropic.ToolUseBlockParam{ ID: "toolu_01A09q90qw90lq917835lq9", Name: "search_knowledge_base", Input: map[string]any{"query": "Acme Dashboard pricing plans"}, }}, ), anthropic.NewUserMessage( anthropic.ContentBlockParamUnion{OfToolResult: &anthropic.ToolResultBlockParam{ ToolUseID: "toolu_01A09q90qw90lq917835lq9", Content: []anthropic.ToolResultBlockParamContentUnion{ {OfSearchResult: &anthropic.SearchResultBlockParam{ Content: []anthropic.TextBlockParam{ {Text: "Acme Dashboard is available on the Starter plan at $10 per user per month and the Enterprise plan with custom pricing."}, }, Source: "https://docs.company.com/pricing", Title: "Pricing Plans", Citations: anthropic.CitationsConfigParam{Enabled: anthropic.Bool(true)}, }}, }, }}, ), }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java import com.anthropic.core.JsonValue; import com.anthropic.models.messages.CitationsConfigParam; import com.anthropic.models.messages.ContentBlockParam; // ... import com.anthropic.models.messages.SearchResultBlockParam; import com.anthropic.models.messages.TextBlockParam; import com.anthropic.models.messages.Tool; import com.anthropic.models.messages.ToolResultBlockParam; import com.anthropic.models.messages.ToolUseBlockParam; // ... void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); Tool knowledgeBaseTool = Tool.builder() .name("search_knowledge_base") .description("Search the company knowledge base for information") .inputSchema(Tool.InputSchema.builder() .properties(JsonValue.from(Map.of( "query", Map.of("type", "string", "description", "The search query") ))) .putAdditionalProperty("required", JsonValue.from(List.of("query"))) .build()) .build(); // Replay a conversation that provides search results both ways: the first // user message carries a pre-fetched result, the tool result returns another MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addTool(knowledgeBaseTool) .addUserMessageOfBlockParams(List.of( ContentBlockParam.ofSearchResult(SearchResultBlockParam.builder() .source("https://docs.company.com/overview") .title("Product Overview") .content(List.of(TextBlockParam.builder() .text("Acme Dashboard is a monitoring tool for distributed systems. It supports real-time alerting and custom metric dashboards.") .build())) .citations(CitationsConfigParam.builder().enabled(true).build()) .build()), ContentBlockParam.ofText(TextBlockParam.builder() .text("What does Acme Dashboard do, and what plans is it available on?") .build()) )) .addAssistantMessageOfBlockParams(List.of( ContentBlockParam.ofText(TextBlockParam.builder() .text("Let me check the pricing information.") .build()), ContentBlockParam.ofToolUse(ToolUseBlockParam.builder() .id("toolu_01A09q90qw90lq917835lq9") .name("search_knowledge_base") .input(JsonValue.from(Map.of("query", "Acme Dashboard pricing plans"))) .build()) )) .addUserMessageOfBlockParams(List.of( ContentBlockParam.ofToolResult(ToolResultBlockParam.builder() .toolUseId("toolu_01A09q90qw90lq917835lq9") .contentOfBlocks(List.of( ToolResultBlockParam.Content.Block.ofSearchResult(SearchResultBlockParam.builder() .source("https://docs.company.com/pricing") .title("Pricing Plans") .content(List.of(TextBlockParam.builder() .text("Acme Dashboard is available on the Starter plan at $10 per user per month and the Enterprise plan with custom pricing.") .build())) .citations(CitationsConfigParam.builder().enabled(true).build()) .build()) )) .build()) )) .build(); Message response = client.messages().create(params); System.out.println(response); } ``` ```php PHP $client = new Client(); $knowledgeBaseTool = [ 'name' => 'search_knowledge_base', 'description' => 'Search the company knowledge base for information', 'input_schema' => [ 'type' => 'object', 'properties' => [ 'query' => ['type' => 'string', 'description' => 'The search query'] ], 'required' => ['query'] ] ]; // Replay a conversation that provides search results both ways: the first // user message carries a pre-fetched result, the tool result returns another $response = $client->messages->create( maxTokens: 1024, tools: [$knowledgeBaseTool], messages: [ [ 'role' => 'user', 'content' => [ [ 'type' => 'search_result', 'source' => 'https://docs.company.com/overview', 'title' => 'Product Overview', 'content' => [ [ 'type' => 'text', 'text' => 'Acme Dashboard is a monitoring tool for distributed systems. It supports real-time alerting and custom metric dashboards.' ] ], 'citations' => ['enabled' => true] ], [ 'type' => 'text', 'text' => 'What does Acme Dashboard do, and what plans is it available on?' ] ] ], [ 'role' => 'assistant', 'content' => [ ['type' => 'text', 'text' => 'Let me check the pricing information.'], [ 'type' => 'tool_use', 'id' => 'toolu_01A09q90qw90lq917835lq9', 'name' => 'search_knowledge_base', 'input' => ['query' => 'Acme Dashboard pricing plans'] ] ] ], [ 'role' => 'user', 'content' => [ [ 'type' => 'tool_result', 'tool_use_id' => 'toolu_01A09q90qw90lq917835lq9', 'content' => [ [ 'type' => 'search_result', 'source' => 'https://docs.company.com/pricing', 'title' => 'Pricing Plans', 'content' => [ [ 'type' => 'text', 'text' => 'Acme Dashboard is available on the Starter plan at $10 per user per month and the Enterprise plan with custom pricing.' ] ], 'citations' => ['enabled' => true] ] ] ] ] ] ], model: 'claude-opus-5', ); echo json_encode($response, JSON_PRETTY_PRINT); ``` ```ruby Ruby client = Anthropic::Client.new knowledge_base_tool = { name: "search_knowledge_base", description: "Search the company knowledge base for information", input_schema: { type: "object", properties: { query: { type: "string", description: "The search query" } }, required: ["query"] } } # Replay a conversation that provides search results both ways: the first # user message carries a pre-fetched result, the tool result returns another response = client.messages.create( model: "claude-opus-5", max_tokens: 1024, tools: [knowledge_base_tool], messages: [ { role: "user", content: [ { type: "search_result", source: "https://docs.company.com/overview", title: "Product Overview", content: [ { type: "text", text: "Acme Dashboard is a monitoring tool for distributed systems. It supports real-time alerting and custom metric dashboards." } ], citations: { enabled: true } }, { type: "text", text: "What does Acme Dashboard do, and what plans is it available on?" } ] }, { role: "assistant", content: [ { type: "text", text: "Let me check the pricing information." }, { type: "tool_use", id: "toolu_01A09q90qw90lq917835lq9", name: "search_knowledge_base", input: { query: "Acme Dashboard pricing plans" } } ] }, { role: "user", content: [ { type: "tool_result", tool_use_id: "toolu_01A09q90qw90lq917835lq9", content: [ { type: "search_result", source: "https://docs.company.com/pricing", title: "Pricing Plans", content: [ { type: "text", text: "Acme Dashboard is available on the Starter plan at $10 per user per month and the Enterprise plan with custom pricing." } ], citations: { enabled: true } } ] } ] } ] ) puts response ``` The response cites both sources. The pre-fetched result is `search_result_index: 0` and the tool-returned result is `search_result_index: 1`, matching the order the `search_result` blocks appear in the conversation: ```json { "role": "assistant", "content": [ { "type": "text", "text": "Here's what I found about Acme Dashboard:\n\n**What it does:** " }, { "type": "text", "text": "Acme Dashboard is a monitoring tool for distributed systems. It supports real-time alerting and custom metric dashboards.", "citations": [ { "type": "search_result_location", "cited_text": "Acme Dashboard is a monitoring tool for distributed systems. It supports real-time alerting and custom metric dashboards.", "source": "https://docs.company.com/overview", "title": "Product Overview", "search_result_index": 0, "start_block_index": 0, "end_block_index": 1 } ] }, { "type": "text", "text": "\n\n**Available plans:** " }, { "type": "text", "text": "Acme Dashboard is available on the Starter plan at $10 per user per month and the Enterprise plan with custom pricing.", "citations": [ { "type": "search_result_location", "cited_text": "Acme Dashboard is available on the Starter plan at $10 per user per month and the Enterprise plan with custom pricing.", "source": "https://docs.company.com/pricing", "title": "Pricing Plans", "search_result_index": 1, "start_block_index": 0, "end_block_index": 1 } ] } ] } ``` ### Mixing with other content types In user messages, `search_result` blocks can sit alongside any other content block. The Method 2 example pairs search results with a `text` question, and image or document blocks can join them the same way. Tool results are stricter: if any block in a `tool_result` content array is a `search_result`, all of its blocks must be `search_result`. Mixing search results with other block types in the same tool result returns a validation error. To return supporting text alongside tool-sourced search results, include it as a text block inside one of the search results' `content` arrays, where it also becomes citable. ### Cache control Add `cache_control` on the search result block to cache it for reuse across requests. It sits alongside `citations` on the same block: ```json { "type": "search_result", "source": "https://docs.company.com/guide", "title": "User Guide", "content": [{ "type": "text", "text": "..." }], "citations": { "enabled": true }, "cache_control": { "type": "ephemeral" } } ``` See [Prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) for minimum cacheable lengths and other requirements. ### Citation control By default, citations are disabled for search results. You can enable citations by explicitly setting the `citations` configuration: ```json { "type": "search_result", "source": "https://docs.company.com/guide", "title": "User Guide", "content": [{ "type": "text", "text": "Important documentation..." }], "citations": { "enabled": true // Enable citations for this result } } ``` When `citations.enabled` is set to `true`, Claude attaches citation references to the text blocks that draw on the search result. Citations are all-or-nothing: either all search results in a request must have citations enabled, or all must have them disabled. Mixing search results with different citation settings results in an error. ## Best practices ### For tool-based search (Method 1) * **Dynamic content:** Use for real-time searches and dynamic RAG applications * **Error handling:** Return appropriate messages when searches fail * **Result limits:** Return only the most relevant results to avoid context overflow ### For top-level search (Method 2) * **Pre-fetched content:** Use when you already have search results * **Batch processing:** Ideal for processing multiple search results at once * **Testing:** Great for testing citation behavior with known content ### General best practices 1. **Structure results effectively:** * Use clear, permanent source URLs * Provide descriptive titles * Break long content into logical text blocks to give Claude finer citation boundaries 2. **Maintain consistency:** * Use consistent source formats across your application * Ensure titles accurately reflect content * Keep formatting consistent 3. **Handle errors gracefully:** when a search fails or returns nothing, return a plain text block describing the outcome (for example, `{"type": "text", "text": "No results found."}`) instead of raising an error: Claude explains the empty result to the user, and the conversation continues. ## Limitations * Search result content blocks are available on Claude API, Amazon Bedrock, and Google Cloud. * Only text content is supported within search results (no images or other media). * `search_result` blocks can only appear in user messages (including inside tool results). Assistant messages with search results are rejected. * When the [web search tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-search-tool) is enabled in the same request, citations must be enabled on all `search_result` blocks. ## Next steps Detect and handle refusal stop reasons in streaming responses, and retry refused requests on a fallback model. Ground Claude's responses in your source documents. Citations return the exact passages that support each claim, so you can verify answers and surface sources to your users. Give Claude access to current web content with cited sources, optional dynamic filtering, and domain controls. See the complete Messages API documentation, including content block types. Cache search results with `cache_control` to reduce cost and latency on repeated requests. --- title: Streaming messages url: https://platform.claude.com/docs/en/build-with-claude/streaming description: Stream Messages API responses incrementally with server-sent events, including text, tool use, and extended thinking deltas. --- When creating a Message, you can set `"stream": true` to incrementally stream the response using [server-sent events](https://developer.mozilla.org/en-US/Web/API/Server-sent%5Fevents/Using%5Fserver-sent%5Fevents) (SSE). ## Streaming with SDKs The [Python SDK](https://github.com/anthropics/anthropic-sdk-python) and [TypeScript SDK](https://github.com/anthropics/anthropic-sdk-typescript) offer multiple ways of streaming. The [PHP SDK](https://github.com/anthropics/anthropic-sdk-php) provides streaming through `createStream()`. The Python SDK allows both sync and async streams. See the documentation in each SDK for details. ```bash CLI ant messages create --stream --format jsonl \ --model claude-opus-5 \ --max-tokens 1024 \ --message '{role: user, content: "Hello"}' \ | jq -rj 'select(.delta.type? == "text_delta") | .delta.text' ``` ```python Python client = anthropic.Anthropic() with client.messages.stream( max_tokens=1024, messages=[{"role": "user", "content": "Hello"}], model="claude-opus-5", ) as stream: for text in stream.text_stream: print(text, end="", flush=True) ``` ```typescript TypeScript const client = new Anthropic(); await client.messages .stream({ messages: [{ role: "user", content: "Hello" }], model: "claude-opus-5", max_tokens: 1024 }) .on("text", (text) => { console.log(text); }); ``` ```csharp C# AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [new() { Role = Role.User, Content = "Hello" }] }; await foreach (var msg in client.Messages.CreateStreaming(parameters)) { Console.Write(msg); } ``` ```go Go client := anthropic.NewClient() stream := client.Messages.NewStreaming(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Hello")), }, }) for stream.Next() { event := stream.Current() switch eventVariant := event.AsAny().(type) { case anthropic.ContentBlockDeltaEvent: switch deltaVariant := eventVariant.Delta.AsAny().(type) { case anthropic.TextDelta: fmt.Print(deltaVariant.Text) } } } if err := stream.Err(); err != nil { log.Fatal(err) } ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addUserMessage("Hello") .build(); try (var streamResponse = client.messages().createStreaming(params)) { streamResponse.stream().forEach(event -> { event.contentBlockDelta().ifPresent(deltaEvent -> deltaEvent.delta().text().ifPresent(td -> System.out.print(td.text()) ) ); }); } ``` ```php PHP $client = new Client(); $stream = $client->messages->createStream( maxTokens: 1024, messages: [ ['role' => 'user', 'content' => 'Hello'] ], model: 'claude-opus-5', ); foreach ($stream as $message) { echo $message; } ``` ```ruby Ruby client = Anthropic::Client.new stream = client.messages.stream( model: "claude-opus-5", max_tokens: 1024, messages: [{ role: "user", content: "Hello" }] ) stream.text.each { |text| print(text) } ``` ## Get the final message without handling events If you don't need to process text as it arrives, the SDKs provide a way to use streaming internally while returning the complete `Message` object, identical to what `.create()` returns. This is especially useful for requests with large `max_tokens` values, where the SDKs require streaming to avoid HTTP timeouts. ```bash CLI # The ant CLI's --stream flag emits one event per line and does not # accumulate into a final Message. For long generations, stream the # raw events: ant messages create --stream --format jsonl <<'YAML' model: claude-opus-5 max_tokens: 128000 messages: - role: user content: Write a detailed analysis... YAML ``` ```python Python client = anthropic.Anthropic() with client.messages.stream( max_tokens=128000, messages=[{"role": "user", "content": "Write a detailed analysis..."}], model="claude-opus-5", ) as stream: message = stream.get_final_message() for block in message.content: if block.type == "text": print(block.text) ``` ```typescript TypeScript const client = new Anthropic(); const stream = client.messages.stream({ max_tokens: 128000, messages: [{ role: "user", content: "Write a detailed analysis..." }], model: "claude-opus-5" }); const message = await stream.finalMessage(); const textBlock = message.content.find((block) => block.type === "text"); if (textBlock && textBlock.type === "text") { console.log(textBlock.text); } ``` ```csharp C# AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 128000, Messages = [new() { Role = Role.User, Content = "Write a detailed analysis..." }] }; var fullText = ""; await foreach (var msg in client.Messages.CreateStreaming(parameters)) { fullText += msg; } Console.WriteLine(fullText); ``` ```go Go client := anthropic.NewClient() stream := client.Messages.NewStreaming(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 128000, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Write a detailed analysis...")), }, }) message := anthropic.Message{} for stream.Next() { event := stream.Current() if err := message.Accumulate(event); err != nil { log.Fatal(err) } } if err := stream.Err(); err != nil { log.Fatal(err) } for _, block := range message.Content { if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok { fmt.Println(textBlock.Text) } } ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(128000L) .addUserMessage("Write a detailed analysis...") .build(); MessageAccumulator accumulator = MessageAccumulator.create(); try (var streamResponse = client.messages().createStreaming(params)) { streamResponse.stream().forEach(accumulator::accumulate); } Message message = accumulator.message(); message.content().stream() .flatMap(block -> block.text().stream()) .forEach(textBlock -> System.out.println(textBlock.text())); ``` ```php PHP $client = new Client(); $stream = $client->messages->createStream( maxTokens: 128000, messages: [ ['role' => 'user', 'content' => 'Write a detailed analysis...'] ], model: 'claude-opus-5', ); $fullText = ''; foreach ($stream as $event) { if ($event->type === 'content_block_delta' && $event->delta->type === 'text_delta') { $fullText .= $event->delta->text; } } echo $fullText; ``` ```ruby Ruby client = Anthropic::Client.new message = client.messages.stream( model: "claude-opus-5", max_tokens: 128000, messages: [{ role: "user", content: "Write a detailed analysis..." }] ).accumulated_message message.content.each do |block| puts block.text if block.type == :text end ``` The `.stream()` call keeps the HTTP connection alive with server-sent events, then `.get_final_message()` (Python) or `.finalMessage()` (TypeScript) accumulates all events and returns the complete `Message` object. In Go, you call `message.Accumulate(event)` inside the stream loop to build the same complete `Message`. In Java, use `MessageAccumulator.create()` and call `accumulator.accumulate(event)` on each event. In C#, await the stream's `.Aggregate()` extension method to get the complete `Message`, or pass a `MessageContentAggregator` to `.CollectAsync()` to aggregate while handling events. In Ruby, call `.accumulated_message` on the stream. In the PHP SDK, you iterate over stream events manually to accumulate the response. ## Event types Each server-sent event includes a named event type and associated JSON data. Each event uses an SSE event name (for example, `event: message_stop`), and includes the matching event `type` in its data. Each stream uses the following event flow: 1. `message_start`: contains a `Message` object with empty `content`. 2. A series of content blocks, each of which has a `content_block_start`, one or more `content_block_delta` events, and a `content_block_stop` event. Each content block has an `index` that corresponds to its index in the final Message `content` array. One exception: during [server-side fallback](https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback#server-side-fallback) responses, a `fallback` content block arrives at each model boundary as a `content_block_start` and `content_block_stop` pair with no deltas in between. 3. One or more `message_delta` events, indicating top-level changes to the final `Message` object. 4. A final `message_stop` event. The token counts shown in the `usage` field of the `message_delta` event are *cumulative*. ### Ping events Event streams may also include any number of `ping` events. ### Error events The API may occasionally send [errors](https://platform.claude.com/docs/en/api/errors) in the event stream. For example, during periods of high usage, you may receive an `overloaded_error`, which would normally correspond to an HTTP 529 in a non-streaming context: ```sse Example error event: error data: {"type": "error", "error": {"type": "overloaded_error", "message": "Overloaded"}} ``` ### Other events In accordance with the [versioning policy](https://platform.claude.com/docs/en/api/versioning), new event types may be added, and your code should handle unknown event types gracefully. ## Content block delta types Each `content_block_delta` event contains a `delta` of a type that updates the `content` block at a given `index`. ### Text delta A `text` content block delta looks like: ```sse Text delta event: content_block_delta data: {"type": "content_block_delta","index": 0,"delta": {"type": "text_delta", "text": "ello frien"}} ``` ### Input JSON delta The deltas for `tool_use` content blocks correspond to updates for the `input` field of the block. To support maximum granularity, the deltas are *partial JSON strings*, whereas the final `tool_use.input` is always an *object*. You can accumulate the string deltas and parse the JSON once you receive a `content_block_stop` event, by using a library like [Pydantic](https://docs.pydantic.dev/latest/concepts/json/#partial-json-parsing) to do partial JSON parsing, or by using the [SDKs](https://platform.claude.com/docs/en/cli-sdks-libraries/overview), which provide helpers to access parsed incremental values. A `tool_use` content block delta looks like: ```sse Input JSON delta event: content_block_delta data: {"type": "content_block_delta","index": 1,"delta": {"type": "input_json_delta","partial_json": "{\"location\": \"San Fra"}}} ``` Note: Current models only support emitting one complete key and value property from `input` at a time. As such, when using tools, there may be delays between streaming events while the model is working. Once an `input` key and value are accumulated, they are emitted as multiple `content_block_delta` events with chunked partial JSON so that the format can automatically support finer granularity in future models. ### Thinking delta When using [thinking](https://platform.claude.com/docs/en/build-with-claude/thinking#streaming-thinking) with streaming enabled, you'll receive thinking content through `thinking_delta` events. These deltas correspond to the `thinking` field of the `thinking` content blocks. For thinking content, a special `signature_delta` event is sent just before the `content_block_stop` event. This signature is used to verify the integrity of the thinking block. When `display: "omitted"` is set on the thinking configuration, no `thinking_delta` events are sent. The thinking block opens, receives a single `signature_delta`, and closes. See [Controlling thinking display](https://platform.claude.com/docs/en/build-with-claude/thinking#controlling-thinking-display). A typical thinking delta looks like: ```sse Thinking delta event: content_block_delta data: {"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_delta", "thinking": "I need to find the GCD of 1071 and 462 using the Euclidean algorithm.\n\n1071 = 2 × 462 + 147"}} ``` The signature delta looks like: ```sse Signature delta event: content_block_delta data: {"type": "content_block_delta", "index": 0, "delta": {"type": "signature_delta", "signature": "EqQBCgIYAhIM1gbcDa9GJwZA2b3hGgxBdjrkzLoky3dl1pkiMOYds..."}} ``` ## Full HTTP stream response Use the [client SDKs](https://platform.claude.com/docs/en/cli-sdks-libraries/overview) when using streaming mode. However, if you are building a direct API integration, you need to handle these events yourself. A stream response consists of: 1. A `message_start` event 2. Potentially multiple content blocks, each of which contains: * A `content_block_start` event * Potentially multiple `content_block_delta` events * A `content_block_stop` event 3. One or more `message_delta` events 4. A `message_stop` event There may be `ping` events dispersed throughout the response as well. See [Event types](https://platform.claude.com/docs/en/build-with-claude/streaming#event-types) for more details on the format. ### Basic streaming request ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -d '{ "model": "claude-opus-5", "messages": [{"role": "user", "content": "Hello"}], "max_tokens": 256, "stream": true }' ``` ```bash CLI ant messages create --stream --format jsonl \ --model claude-opus-5 \ --max-tokens 256 \ --message '{role: user, content: Hello}' ``` ```python Python client = anthropic.Anthropic() with client.messages.stream( model="claude-opus-5", messages=[{"role": "user", "content": "Hello"}], max_tokens=256, ) as stream: for text in stream.text_stream: print(text, end="", flush=True) ``` ```typescript TypeScript const client = new Anthropic(); const stream = client.messages.stream({ model: "claude-opus-5", messages: [{ role: "user", content: "Hello" }], max_tokens: 256 }); for await (const event of stream) { if (event.type === "content_block_delta" && event.delta.type === "text_delta") { process.stdout.write(event.delta.text); } } ``` ```csharp C# AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 256, Messages = [new() { Role = Role.User, Content = "Hello" }] }; await foreach (var msg in client.Messages.CreateStreaming(parameters)) { Console.Write(msg); } ``` ```go Go client := anthropic.NewClient() stream := client.Messages.NewStreaming(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 256, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Hello")), }, }) for stream.Next() { event := stream.Current() switch eventVariant := event.AsAny().(type) { case anthropic.ContentBlockDeltaEvent: switch deltaVariant := eventVariant.Delta.AsAny().(type) { case anthropic.TextDelta: fmt.Print(deltaVariant.Text) } } } if err := stream.Err(); err != nil { log.Fatal(err) } ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(256L) .addUserMessage("Hello") .build(); try (var streamResponse = client.messages().createStreaming(params)) { streamResponse.stream().forEach(event -> { event.contentBlockDelta().ifPresent(deltaEvent -> deltaEvent.delta().text().ifPresent(td -> System.out.print(td.text()) ) ); }); } ``` ```php PHP $client = new Client(); $stream = $client->messages->createStream( maxTokens: 256, messages: [ ['role' => 'user', 'content' => 'Hello'] ], model: 'claude-opus-5', ); foreach ($stream as $message) { echo $message; } ``` ```ruby Ruby client = Anthropic::Client.new stream = client.messages.stream( model: "claude-opus-5", messages: [{ role: "user", content: "Hello" }], max_tokens: 256 ) stream.text.each { |text| print(text) } ``` ```sse Response event: message_start data: {"type": "message_start", "message": {"id": "msg_1nZdL29xx5MUA1yADyHTEsnR8uuvGzszyY", "type": "message", "role": "assistant", "content": [], "model": "claude-opus-5", "stop_reason": null, "stop_sequence": null, "usage": {"input_tokens": 25, "output_tokens": 1}}} event: content_block_start data: {"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}} event: ping data: {"type": "ping"} event: content_block_delta data: {"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "Hello"}} event: content_block_delta data: {"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "!"}} event: content_block_stop data: {"type": "content_block_stop", "index": 0} event: message_delta data: {"type": "message_delta", "delta": {"stop_reason": "end_turn", "stop_sequence":null}, "usage": {"output_tokens": 15}} event: message_stop data: {"type": "message_stop"} ``` ### Streaming request with tool use Tool use supports [fine-grained streaming](https://platform.claude.com/docs/en/agents-and-tools/tool-use/fine-grained-tool-streaming) for parameter values. Enable it per tool with `eager_input_streaming`. This request asks Claude to use a tool to report the weather. ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "tools": [ { "name": "get_weather", "description": "Get the current weather in a given location", "input_schema": { "type": "object", "properties": { "location": { "type": "string", "description": "The city and state, e.g. San Francisco, CA" } }, "required": ["location"] } } ], "tool_choice": {"type": "any"}, "messages": [ { "role": "user", "content": "What is the weather like in San Francisco?" } ], "stream": true }' ``` ```bash CLI ant messages create --stream --format jsonl <<'YAML' model: claude-opus-5 max_tokens: 1024 tools: - name: get_weather description: Get the current weather in a given location input_schema: type: object properties: location: type: string description: The city and state, e.g. San Francisco, CA required: - location tool_choice: type: any messages: - role: user content: What is the weather like in San Francisco? YAML ``` ```python Python client = anthropic.Anthropic() tools = [ { "name": "get_weather", "description": "Get the current weather in a given location", "input_schema": { "type": "object", "properties": { "location": { "type": "string", "description": "The city and state, e.g. San Francisco, CA", } }, "required": ["location"], }, } ] with client.messages.stream( model="claude-opus-5", max_tokens=1024, tools=tools, tool_choice={"type": "any"}, messages=[ {"role": "user", "content": "What is the weather like in San Francisco?"} ], ) as stream: for text in stream.text_stream: print(text, end="", flush=True) ``` ```typescript TypeScript const client = new Anthropic(); const tools: Anthropic.Tool[] = [ { name: "get_weather", description: "Get the current weather in a given location", input_schema: { type: "object", properties: { location: { type: "string", description: "The city and state, e.g. San Francisco, CA" } }, required: ["location"] } } ]; const stream = client.messages.stream({ model: "claude-opus-5", max_tokens: 1024, tools: tools, tool_choice: { type: "any" }, messages: [ { role: "user", content: "What is the weather like in San Francisco?" } ] }); for await (const event of stream) { if (event.type === "content_block_delta" && event.delta.type === "text_delta") { process.stdout.write(event.delta.text); } } ``` ```csharp C# AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Tools = [ new ToolUnion(new Tool() { Name = "get_weather", Description = "Get the current weather in a given location", InputSchema = new InputSchema() { Properties = new Dictionary { ["location"] = JsonSerializer.SerializeToElement(new { type = "string", description = "The city and state, e.g. San Francisco, CA" }), }, Required = ["location"], }, }), ], ToolChoice = new ToolChoiceAny(), Messages = [ new() { Role = Role.User, Content = "What is the weather like in San Francisco?" } ] }; await foreach (var msg in client.Messages.CreateStreaming(parameters)) { Console.Write(msg); } ``` ```go Go client := anthropic.NewClient() stream := client.Messages.NewStreaming(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Tools: []anthropic.ToolUnionParam{ {OfTool: &anthropic.ToolParam{ Name: "get_weather", Description: anthropic.String("Get the current weather in a given location"), InputSchema: anthropic.ToolInputSchemaParam{ Properties: map[string]any{ "location": map[string]any{ "type": "string", "description": "The city and state, e.g. San Francisco, CA", }, }, Required: []string{"location"}, }, }}, }, ToolChoice: anthropic.ToolChoiceUnionParam{OfAny: &anthropic.ToolChoiceAnyParam{}}, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("What is the weather like in San Francisco?")), }, }) for stream.Next() { event := stream.Current() switch eventVariant := event.AsAny().(type) { case anthropic.ContentBlockDeltaEvent: switch deltaVariant := eventVariant.Delta.AsAny().(type) { case anthropic.TextDelta: fmt.Print(deltaVariant.Text) } } } if err := stream.Err(); err != nil { log.Fatal(err) } ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addTool(Tool.builder() .name("get_weather") .description("Get the current weather in a given location") .inputSchema(Tool.InputSchema.builder() .properties(JsonValue.from(Map.of( "location", Map.of( "type", "string", "description", "The city and state, e.g. San Francisco, CA" ) ))) .putAdditionalProperty("required", JsonValue.from(List.of("location"))) .build()) .build()) .toolChoice(ToolChoice.ofAny(ToolChoiceAny.builder().build())) .addUserMessage("What is the weather like in San Francisco?") .build(); try (var streamResponse = client.messages().createStreaming(params)) { streamResponse.stream().forEach(event -> { event.contentBlockDelta().ifPresent(deltaEvent -> deltaEvent.delta().text().ifPresent(td -> System.out.print(td.text()) ) ); }); } ``` ```php PHP $client = new Client(); $stream = $client->messages->createStream( maxTokens: 1024, messages: [ ['role' => 'user', 'content' => 'What is the weather like in San Francisco?'] ], model: 'claude-opus-5', toolChoice: ['type' => 'any'], tools: [ [ 'name' => 'get_weather', 'description' => 'Get the current weather in a given location', 'input_schema' => [ 'type' => 'object', 'properties' => [ 'location' => [ 'type' => 'string', 'description' => 'The city and state, e.g. San Francisco, CA' ] ], 'required' => ['location'] ] ] ], ); foreach ($stream as $message) { echo $message; } ``` ```ruby Ruby client = Anthropic::Client.new tools = [ { name: "get_weather", description: "Get the current weather in a given location", input_schema: { type: "object", properties: { location: { type: "string", description: "The city and state, e.g. San Francisco, CA" } }, required: ["location"] } } ] stream = client.messages.stream( model: "claude-opus-5", max_tokens: 1024, tools: tools, tool_choice: { type: "any" }, messages: [ { role: "user", content: "What is the weather like in San Francisco?" } ] ) stream.text.each { |text| print(text) } ``` ```sse Response event: message_start data: {"type":"message_start","message":{"id":"msg_014p7gG3wDgGV9EUtLvnow3U","type":"message","role":"assistant","model":"claude-opus-5","stop_sequence":null,"usage":{"input_tokens":472,"output_tokens":2},"content":[],"stop_reason":null}} event: content_block_start data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}} event: ping data: {"type": "ping"} event: content_block_delta data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Okay"}} event: content_block_delta data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":","}} event: content_block_delta data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" let"}} event: content_block_delta data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"'s"}} event: content_block_delta data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" check"}} event: content_block_delta data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" the"}} event: content_block_delta data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" weather"}} event: content_block_delta data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" for"}} event: content_block_delta data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" San"}} event: content_block_delta data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Francisco"}} event: content_block_delta data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":","}} event: content_block_delta data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" CA"}} event: content_block_delta data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":":"}} event: content_block_stop data: {"type":"content_block_stop","index":0} event: content_block_start data: {"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_01T1x1fJ34qAmk2tNTrN7Up6","name":"get_weather","input":{}}} event: content_block_delta data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":""}} event: content_block_delta data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"location\":"}} event: content_block_delta data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":" \"San"}} event: content_block_delta data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":" Francisc"}} event: content_block_delta data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"o,"}} event: content_block_delta data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":" CA\"}"}} event: content_block_stop data: {"type":"content_block_stop","index":1} event: message_delta data: {"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"output_tokens":89}} event: message_stop data: {"type":"message_stop"} ``` ### Streaming request with thinking This request enables thinking with streaming. The `display: "summarized"` setting streams a condensed summary of Claude's reasoning rather than the full chain of thought. ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 20000, "stream": true, "thinking": { "type": "adaptive", "display": "summarized" }, "messages": [ { "role": "user", "content": "What is the greatest common divisor of 1071 and 462?" } ] }' ``` ```bash CLI ant messages create --stream --format jsonl \ --model claude-opus-5 \ --max-tokens 20000 \ --thinking '{type: adaptive, display: summarized}' \ --message '{role: user, content: What is the greatest common divisor of 1071 and 462?}' ``` ```python Python client = anthropic.Anthropic() with client.messages.stream( model="claude-opus-5", max_tokens=20000, thinking={"type": "adaptive", "display": "summarized"}, messages=[ { "role": "user", "content": "What is the greatest common divisor of 1071 and 462?", } ], ) as stream: for event in stream: if event.type == "content_block_delta": if event.delta.type == "thinking_delta": print(event.delta.thinking, end="", flush=True) elif event.delta.type == "text_delta": print(event.delta.text, end="", flush=True) ``` ```typescript TypeScript const client = new Anthropic(); const stream = client.messages.stream({ model: "claude-opus-5", max_tokens: 20000, thinking: { type: "adaptive", display: "summarized" }, messages: [ { role: "user", content: "What is the greatest common divisor of 1071 and 462?" } ] }); for await (const event of stream) { if (event.type === "content_block_delta") { if (event.delta.type === "thinking_delta") { process.stdout.write(event.delta.thinking); } else if (event.delta.type === "text_delta") { process.stdout.write(event.delta.text); } } } ``` ```csharp C# using Anthropic; using Anthropic.Models.Messages; AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 20000, Thinking = new ThinkingConfigAdaptive { Display = Display.Summarized }, Messages = [new() { Role = Role.User, Content = "What is the greatest common divisor of 1071 and 462?" }] }; await foreach (var msg in client.Messages.CreateStreaming(parameters)) { Console.Write(msg); } ``` ```go Go client := anthropic.NewClient() stream := client.Messages.NewStreaming(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 20000, Thinking: anthropic.ThinkingConfigParamUnion{ OfAdaptive: &anthropic.ThinkingConfigAdaptiveParam{ Display: anthropic.ThinkingConfigAdaptiveDisplaySummarized, }, }, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("What is the greatest common divisor of 1071 and 462?")), }, }) for stream.Next() { event := stream.Current() switch eventVariant := event.AsAny().(type) { case anthropic.ContentBlockDeltaEvent: switch deltaVariant := eventVariant.Delta.AsAny().(type) { case anthropic.ThinkingDelta: fmt.Print(deltaVariant.Thinking) case anthropic.TextDelta: fmt.Print(deltaVariant.Text) } } } if err := stream.Err(); err != nil { log.Fatal(err) } ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(20000L) .thinking(ThinkingConfigAdaptive.builder() .display(ThinkingConfigAdaptive.Display.SUMMARIZED) .build()) .addUserMessage("What is the greatest common divisor of 1071 and 462?") .build(); try (var streamResponse = client.messages().createStreaming(params)) { streamResponse.stream().forEach(event -> { event.contentBlockDelta().ifPresent(deltaEvent -> { deltaEvent.delta().thinking().ifPresent(td -> IO.print(td.thinking()) ); deltaEvent.delta().text().ifPresent(td -> IO.print(td.text()) ); }); }); } ``` ```php PHP $client = new Client(); $stream = $client->messages->createStream( maxTokens: 20000, messages: [ ['role' => 'user', 'content' => 'What is the greatest common divisor of 1071 and 462?'] ], model: 'claude-opus-5', thinking: ['type' => 'adaptive', 'display' => 'summarized'], ); foreach ($stream as $message) { echo $message; } ``` ```ruby Ruby client = Anthropic::Client.new stream = client.messages.stream( model: "claude-opus-5", max_tokens: 20000, thinking: { type: "adaptive", display: "summarized" }, messages: [ { role: "user", content: "What is the greatest common divisor of 1071 and 462?" } ] ) stream.each do |event| if event.type == :content_block_delta if event.delta.type == :thinking_delta print(event.delta.thinking) elsif event.delta.type == :text_delta print(event.delta.text) end end end ``` ```sse Response event: message_start data: {"type": "message_start", "message": {"id": "msg_01...", "type": "message", "role": "assistant", "content": [], "model": "claude-opus-5", "stop_reason": null, "stop_sequence": null}} event: content_block_start data: {"type": "content_block_start", "index": 0, "content_block": {"type": "thinking", "thinking": "", "signature": ""}} event: content_block_delta data: {"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_delta", "thinking": "I need to find the GCD of 1071 and 462 using the Euclidean algorithm.\n\n1071 = 2 × 462 + 147"}} event: content_block_delta data: {"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_delta", "thinking": "\n462 = 3 × 147 + 21"}} event: content_block_delta data: {"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_delta", "thinking": "\n147 = 7 × 21 + 0"}} event: content_block_delta data: {"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_delta", "thinking": "\nThe remainder is 0, so GCD(1071, 462) = 21."}} event: content_block_delta data: {"type": "content_block_delta", "index": 0, "delta": {"type": "signature_delta", "signature": "EqQBCgIYAhIM1gbcDa9GJwZA2b3hGgxBdjrkzLoky3dl1pkiMOYds..."}} event: content_block_stop data: {"type": "content_block_stop", "index": 0} event: content_block_start data: {"type": "content_block_start", "index": 1, "content_block": {"type": "text", "text": ""}} event: content_block_delta data: {"type": "content_block_delta", "index": 1, "delta": {"type": "text_delta", "text": "The greatest common divisor of 1071 and 462 is **21**."}} event: content_block_stop data: {"type": "content_block_stop", "index": 1} event: message_delta data: {"type": "message_delta", "delta": {"stop_reason": "end_turn", "stop_sequence": null}} event: message_stop data: {"type": "message_stop"} ``` ### Streaming request with web search tool use This request asks Claude to search the web for current weather information. ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "stream": true, "tools": [ { "type": "web_search_20250305", "name": "web_search", "max_uses": 5 } ], "messages": [ { "role": "user", "content": "What is the weather like in New York City today?" } ] }' ``` ```bash CLI ant messages create --stream --format jsonl \ --model claude-opus-5 \ --max-tokens 1024 \ --tool '{type: web_search_20250305, name: web_search, max_uses: 5}' \ --message '{role: user, content: What is the weather like in New York City today?}' ``` ```python Python client = anthropic.Anthropic() with client.messages.stream( model="claude-opus-5", max_tokens=1024, tools=[{"type": "web_search_20250305", "name": "web_search", "max_uses": 5}], messages=[ {"role": "user", "content": "What is the weather like in New York City today?"} ], ) as stream: for text in stream.text_stream: print(text, end="", flush=True) ``` ```typescript TypeScript const client = new Anthropic(); const stream = client.messages.stream({ model: "claude-opus-5", max_tokens: 1024, tools: [{ type: "web_search_20250305", name: "web_search", max_uses: 5 }], messages: [{ role: "user", content: "What is the weather like in New York City today?" }] }); for await (const event of stream) { if (event.type === "content_block_delta" && event.delta.type === "text_delta") { process.stdout.write(event.delta.text); } } ``` ```csharp C# using Anthropic; using Anthropic.Models.Messages; AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Tools = [new ToolUnion(new WebSearchTool20250305() { MaxUses = 5 })], Messages = [new() { Role = Role.User, Content = "What is the weather like in New York City today?" }] }; await foreach (var msg in client.Messages.CreateStreaming(parameters)) { Console.Write(msg); } ``` ```go Go client := anthropic.NewClient() stream := client.Messages.NewStreaming(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Tools: []anthropic.ToolUnionParam{ { OfWebSearchTool20250305: &anthropic.WebSearchTool20250305Param{ MaxUses: anthropic.Int(5), }, }, }, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("What is the weather like in New York City today?")), }, }) for stream.Next() { event := stream.Current() switch eventVariant := event.AsAny().(type) { case anthropic.ContentBlockDeltaEvent: switch deltaVariant := eventVariant.Delta.AsAny().(type) { case anthropic.TextDelta: fmt.Print(deltaVariant.Text) } } } if err := stream.Err(); err != nil { log.Fatal(err) } ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addTool(WebSearchTool20250305.builder() .maxUses(5L) .build()) .addUserMessage("What is the weather like in New York City today?") .build(); try (var streamResponse = client.messages().createStreaming(params)) { streamResponse.stream().forEach(event -> { event.contentBlockDelta().ifPresent(deltaEvent -> deltaEvent.delta().text().ifPresent(td -> System.out.print(td.text()) ) ); }); } ``` ```php PHP $client = new Client(); $stream = $client->messages->createStream( maxTokens: 1024, messages: [ ['role' => 'user', 'content' => 'What is the weather like in New York City today?'] ], model: 'claude-opus-5', tools: [ ['type' => 'web_search_20250305', 'name' => 'web_search', 'max_uses' => 5] ], ); foreach ($stream as $message) { echo $message; } ``` ```ruby Ruby client = Anthropic::Client.new stream = client.messages.stream( model: :"claude-opus-5", max_tokens: 1024, tools: [ { type: "web_search_20250305", name: "web_search", max_uses: 5 } ], messages: [ { role: "user", content: "What is the weather like in New York City today?" } ] ) stream.text.each { |text| print(text) } ``` ```sse Response event: message_start data: {"type":"message_start","message":{"id":"msg_01G...","type":"message","role":"assistant","model":"claude-opus-5","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":2679,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":3}}} event: content_block_start data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}} event: content_block_delta data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"I'll check"}} event: content_block_delta data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" the current weather in New York City for you"}} event: ping data: {"type": "ping"} event: content_block_delta data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"."}} event: content_block_stop data: {"type":"content_block_stop","index":0} event: content_block_start data: {"type":"content_block_start","index":1,"content_block":{"type":"server_tool_use","id":"srvtoolu_014hJH82Qum7Td6UV8gDXThB","name":"web_search","input":{}}} event: content_block_delta data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":""}} event: content_block_delta data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"query"}} event: content_block_delta data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"\":"}} event: content_block_delta data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":" \"weather"}} event: content_block_delta data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":" NY"}} event: content_block_delta data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"C to"}} event: content_block_delta data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"day\"}"}} event: content_block_stop data: {"type":"content_block_stop","index":1 } event: content_block_start data: {"type":"content_block_start","index":2,"content_block":{"type":"web_search_tool_result","tool_use_id":"srvtoolu_014hJH82Qum7Td6UV8gDXThB","content":[{"type":"web_search_result","title":"Weather in New York City in May 2025 (New York) - detailed Weather Forecast for a month","url":"https://world-weather.info/forecast/usa/new_york/may-2025/","encrypted_content":"Ev0DCioIAxgCIiQ3NmU4ZmI4OC1k...","page_age":null},...]}} event: content_block_stop data: {"type":"content_block_stop","index":2} event: content_block_start data: {"type":"content_block_start","index":3,"content_block":{"type":"text","text":""}} event: content_block_delta data: {"type":"content_block_delta","index":3,"delta":{"type":"text_delta","text":"Here's the current weather information for New York"}} event: content_block_delta data: {"type":"content_block_delta","index":3,"delta":{"type":"text_delta","text":" City:\n\n# Weather"}} event: content_block_delta data: {"type":"content_block_delta","index":3,"delta":{"type":"text_delta","text":" in New York City"}} event: content_block_delta data: {"type":"content_block_delta","index":3,"delta":{"type":"text_delta","text":"\n\n"}} ... event: content_block_stop data: {"type":"content_block_stop","index":17} event: message_delta data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":10682,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":510,"server_tool_use":{"web_search_requests":1}}} event: message_stop data: {"type":"message_stop"} ``` ## Error recovery ### Claude 4.5 and earlier For Claude 4.5 models and earlier, you can recover a streaming request that was interrupted because of network issues, timeouts, or other errors by resuming from where the stream was interrupted. This approach saves you from re-processing the entire response. The basic recovery strategy involves: 1. **Capture the partial response:** Save all content that was successfully received before the error occurred. 2. **Construct a continuation request:** Create a new API request that includes the partial assistant response as the beginning of a new assistant message. 3. **Resume streaming:** Continue receiving the rest of the response from where it was interrupted. ### Claude 4.6 and later For Claude 4.6 and later models, the same capture-and-resume strategy applies, but step 2 changes: instead of placing the partial response in an assistant message, add a user message that instructs the model to continue from where it left off. 1. **Capture the partial response:** Save all content that was successfully received before the error occurred. 2. **Construct a continuation request:** Create a new API request with a user message containing the partial response and an instruction to continue, for example: ```text Sample prompt wrap Your previous response was interrupted and ended with [previous_response]. Continue from where you left off. ``` 3. **Resume streaming:** Continue receiving the rest of the response from where it was interrupted. ### Error recovery best practices 1. **Use SDK features:** Leverage the SDK's built-in message accumulation and error handling capabilities. 2. **Handle content types:** Be aware that messages can contain multiple content blocks (`text`, `tool_use`, `thinking`). Tool use and extended thinking blocks cannot be partially recovered. You can resume streaming from the most recent text block. ## Next steps Handle each `stop_reason` value once a stream completes. Stream tool input JSON without server-side buffering for lower latency. Stream thinking output with `thinking_delta` and `signature_delta` events. Use the official SDKs, which handle streaming, accumulation, and reconnection for you. Process large volumes of requests asynchronously when you don't need real-time responses. --- title: Structured outputs url: https://platform.claude.com/docs/en/build-with-claude/structured-outputs description: Get validated JSON results from agent workflows --- ## Compatibility - [ZDR](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention): eligible (excludes [Covered Models](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#model-specific-data-retention-requirements)) - Supported models: `claude-fable-5`, `claude-mythos-5`, `claude-mythos-preview`, `claude-opus-5`, `claude-opus-4-8`, `claude-opus-4-7`, `claude-opus-4-6`, `claude-sonnet-5`, `claude-sonnet-4-6`, `claude-sonnet-4-5-20250929`, `claude-opus-4-5-20251101`, `claude-haiku-4-5-20251001` - Platforms: Claude API, Claude Platform on AWS, Amazon Bedrock [1], Google Cloud, Microsoft Foundry [2] 1. On Amazon Bedrock, structured outputs are available for Claude Opus 4.6, Claude Sonnet 4.6, Claude Sonnet 4.5, Claude Opus 4.5, and Claude Haiku 4.5. 2. On [Microsoft Foundry](https://platform.claude.com/docs/en/build-with-claude/claude-in-microsoft-foundry), structured outputs require a [Hosted on Anthropic deployment](https://platform.claude.com/docs/en/build-with-claude/claude-in-microsoft-foundry#additional-features-not-supported-when-hosted-on-azure). Structured outputs constrain Claude's responses to follow a specific schema, ensuring valid, parseable output for downstream processing. Structured outputs provide two complementary features: * **JSON outputs** (`output_config.format`): Get Claude's response in a specific JSON format * **Strict tool use** (`strict: true`): Guarantee schema validation on tool names and inputs You can use these features independently or together in the same request. **Migrating from beta?** The `output_format` parameter has moved to `output_config.format`, and beta headers are no longer required. The old beta header (`structured-outputs-2025-11-13`) and `output_format` parameter will continue working for a transition period. See the following code examples for the updated API shape. ## Why use structured outputs Without structured outputs, Claude can generate malformed JSON responses or invalid tool inputs that break your applications. Even with careful prompting, you may encounter: * Parsing errors from invalid JSON syntax * Missing required fields * Inconsistent data types * Schema violations requiring error handling and retries Structured outputs guarantee schema-compliant responses through constrained decoding: * **Always valid:** No more `JSON.parse()` errors * **Type safe:** Guaranteed field types and required fields * **Reliable:** No retries needed for schema violations ## JSON outputs JSON outputs control Claude's response format, ensuring Claude returns valid JSON matching your schema. Use JSON outputs when you need to: * Control Claude's response format * Extract data from images or text * Generate structured reports * Format API responses ### Quick start ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "messages": [ { "role": "user", "content": "Extract the key information from this email: John Smith (john@example.com) is interested in our Enterprise plan and wants to schedule a demo for next Tuesday at 2pm." } ], "output_config": { "format": { "type": "json_schema", "schema": { "type": "object", "properties": { "name": {"type": "string"}, "email": {"type": "string"}, "plan_interest": {"type": "string"}, "demo_requested": {"type": "boolean"} }, "required": ["name", "email", "plan_interest", "demo_requested"], "additionalProperties": false } } } }' ``` ```bash CLI ant messages create \ --transform 'content.#(type=="text").text|@fromstr' \ --format jsonl <<'YAML' model: claude-opus-5 max_tokens: 1024 messages: - role: user content: >- Extract the key information from this email: John Smith (john@example.com) is interested in our Enterprise plan and wants to schedule a demo for next Tuesday at 2pm. output_config: format: type: json_schema schema: type: object properties: name: {type: string} email: {type: string} plan_interest: {type: string} demo_requested: {type: boolean} required: [name, email, plan_interest, demo_requested] additionalProperties: false YAML ``` ```python Python client = anthropic.Anthropic() response = client.messages.create( model="claude-opus-5", max_tokens=1024, messages=[ { "role": "user", "content": "Extract the key information from this email: John Smith (john@example.com) is interested in our Enterprise plan and wants to schedule a demo for next Tuesday at 2pm.", } ], output_config={ "format": { "type": "json_schema", "schema": { "type": "object", "properties": { "name": {"type": "string"}, "email": {"type": "string"}, "plan_interest": {"type": "string"}, "demo_requested": {"type": "boolean"}, }, "required": ["name", "email", "plan_interest", "demo_requested"], "additionalProperties": False, }, } }, ) print(next(block.text for block in response.content if block.type == "text")) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: "Extract the key information from this email: John Smith (john@example.com) is interested in our Enterprise plan and wants to schedule a demo for next Tuesday at 2pm." } ], output_config: { format: { type: "json_schema", schema: { type: "object", properties: { name: { type: "string" }, email: { type: "string" }, plan_interest: { type: "string" }, demo_requested: { type: "boolean" } }, required: ["name", "email", "plan_interest", "demo_requested"], additionalProperties: false } } } }); for (const block of response.content) { if (block.type === "text") { console.log(block.text); } } ``` ```csharp C# using System.Text.Json; using Anthropic; using Anthropic.Models.Messages; AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [new() { Role = Role.User, Content = "Extract the key information from this email: John Smith (john@example.com) is interested in our Enterprise plan." }], OutputConfig = new OutputConfig { Format = new JsonOutputFormat { Schema = new Dictionary { ["type"] = JsonSerializer.SerializeToElement("object"), ["properties"] = JsonSerializer.SerializeToElement(new { name = new { type = "string" }, email = new { type = "string" }, plan_interest = new { type = "string" }, demo_requested = new { type = "boolean" }, }), ["required"] = JsonSerializer.SerializeToElement(new[] { "name", "email", "plan_interest", "demo_requested" }), ["additionalProperties"] = JsonSerializer.SerializeToElement(false), }, }, }, }; var message = await client.Messages.Create(parameters); Console.WriteLine(message); ``` ```go Go client := anthropic.NewClient() response, _ := client.Messages.New(context.Background(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage( anthropic.NewTextBlock("Extract the key information from this email: John Smith (john@example.com) is interested in our Enterprise plan."), ), }, OutputConfig: anthropic.OutputConfigParam{ Format: anthropic.JSONOutputFormatParam{ Schema: map[string]any{ "type": "object", "properties": map[string]any{ "name": map[string]string{"type": "string"}, "email": map[string]string{"type": "string"}, "plan_interest": map[string]string{"type": "string"}, "demo_requested": map[string]string{"type": "boolean"}, }, "required": []string{"name", "email", "plan_interest", "demo_requested"}, "additionalProperties": false, }, }, }, }) for _, block := range response.Content { if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok { fmt.Println(textBlock.Text) break } } ``` ```java Java static class ContactInfo { public String name; public String email; public String plan_interest; public boolean demo_requested; } void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); StructuredMessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .addUserMessage("Extract the key information from this email: John Smith (john@example.com) is interested in our Enterprise plan.") .outputConfig(ContactInfo.class) .build(); StructuredMessage response = client.messages().create(params); ContactInfo contact = response.content().stream() .flatMap(block -> block.text().stream()) .findFirst().orElseThrow().text(); IO.println(contact.name + " (" + contact.email + ")"); } ``` ```php PHP $client = new Client(); $response = $client->messages->create( maxTokens: 1024, messages: [ [ 'role' => 'user', 'content' => 'Extract the key information from this email: John Smith (john@example.com) is interested in our Enterprise plan.' ] ], model: 'claude-opus-5', outputConfig: [ 'format' => [ 'type' => 'json_schema', 'schema' => [ 'type' => 'object', 'properties' => [ 'name' => ['type' => 'string'], 'email' => ['type' => 'string'], 'plan_interest' => ['type' => 'string'], 'demo_requested' => ['type' => 'boolean'] ], 'required' => ['name', 'email', 'plan_interest', 'demo_requested'], 'additionalProperties' => false ] ] ], ); $textBlock = array_find($response->content, static fn ($block): bool => $block->type === 'text'); echo $textBlock->text; ``` ```ruby Ruby client = Anthropic::Client.new response = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: "Extract the key information from this email: John Smith (john@example.com) is interested in our Enterprise plan." } ], output_config: { format: { type: "json_schema", schema: { type: "object", properties: { name: { type: "string" }, email: { type: "string" }, plan_interest: { type: "string" }, demo_requested: { type: "boolean" } }, required: ["name", "email", "plan_interest", "demo_requested"], additionalProperties: false } } } ) puts response.content.find { it.type == :text }.text ``` **Response format:** Valid JSON matching your schema in the response's text content block ```json Output { "name": "John Smith", "email": "john@example.com", "plan_interest": "Enterprise", "demo_requested": true } ``` ### How it works Create a JSON schema that describes the structure you want Claude to follow. The schema uses standard JSON Schema format with some limitations (see [JSON Schema limitations](https://platform.claude.com/docs/en/build-with-claude/structured-outputs#json-schema-limitations)). Include the `output_config.format` parameter in your API request with `type: "json_schema"` and your schema definition. Claude's response is valid JSON matching your schema, returned in the response's text content block. ### Working with JSON outputs in SDKs The SDKs provide helpers that make it easier to work with JSON outputs, including schema transformation, automatic validation, and integration with popular schema libraries. The Python SDK's `client.messages.parse()` still accepts `output_format` as a convenience parameter and translates it to `output_config.format` internally. Other SDKs require `output_config` directly. The following examples show the SDK helper syntax. #### Using native schema definitions Instead of writing raw JSON schemas, you can use familiar schema definition tools in your language: * **Python:** [Pydantic](https://docs.pydantic.dev/) models with `client.messages.parse()` * **TypeScript:** [Zod](https://zod.dev/) schemas with `zodOutputFormat()` or typed JSON Schema literals with `jsonSchemaOutputFormat()` * **Java:** Plain Java classes with automatic schema derivation through `outputConfig(Class)` * **Ruby:** `Anthropic::BaseModel` classes with `output_config: {format: Model}` * **PHP:** Classes implementing `StructuredOutputModel` with `outputConfig: ['format' => MyClass::class]` * **C#:** Plain C# classes with the generic `Create()` overload, which derives the schema automatically * **Go:** Go structs reflected into JSON schemas automatically on the beta API, or raw JSON schemas through `output_config` * **CLI:** Raw JSON schemas passed through `output_config` ```bash CLI ant messages create \ --transform 'content.#(type=="text").text|@fromstr|{name,email}' \ --format yaml <<'YAML' model: claude-opus-5 max_tokens: 1024 messages: - role: user content: >- Extract the key information from this email: John Smith (john@example.com) is interested in our Enterprise plan and wants to schedule a demo for next Tuesday at 2pm. output_config: format: type: json_schema schema: type: object properties: name: {type: string} email: {type: string} plan_interest: {type: string} demo_requested: {type: boolean} required: [name, email, plan_interest, demo_requested] additionalProperties: false YAML ``` ```python Python from pydantic import BaseModel from anthropic import Anthropic class ContactInfo(BaseModel): name: str email: str plan_interest: str demo_requested: bool client = Anthropic() response = client.messages.parse( model="claude-opus-5", max_tokens=1024, messages=[ { "role": "user", "content": "Extract the key information from this email: John Smith (john@example.com) is interested in our Enterprise plan and wants to schedule a demo for next Tuesday at 2pm.", } ], output_format=ContactInfo, ) print(response.parsed_output) ``` ```typescript TypeScript import { z } from "zod"; import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod"; const ContactInfoSchema = z.object({ name: z.string(), email: z.string(), plan_interest: z.string(), demo_requested: z.boolean() }); const client = new Anthropic(); const response = await client.messages.parse({ model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: "Extract the key information from this email: John Smith (john@example.com) is interested in our Enterprise plan and wants to schedule a demo for next Tuesday at 2pm." } ], output_config: { format: zodOutputFormat(ContactInfoSchema) } }); // Automatically parsed and validated console.log(response.parsed_output); ``` ```csharp C# using System.Text.Json; using Anthropic; using Anthropic.Models.Messages; var client = new AnthropicClient(); var response = await client.Messages.Create(new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [new() { Role = Role.User, Content = "Extract the key information from this email: John Smith (john@example.com) is interested in our Enterprise plan and wants to schedule a demo for next Tuesday at 2pm." }], OutputConfig = new OutputConfig { Format = new JsonOutputFormat { Schema = new Dictionary { ["type"] = JsonSerializer.SerializeToElement("object"), ["properties"] = JsonSerializer.SerializeToElement(new { name = new { type = "string" }, email = new { type = "string" }, plan_interest = new { type = "string" }, demo_requested = new { type = "boolean" }, }), ["required"] = JsonSerializer.SerializeToElement( new[] { "name", "email", "plan_interest", "demo_requested" }), ["additionalProperties"] = JsonSerializer.SerializeToElement(false), }, }, }, }); if (response.Content.Select(b => b.Value).OfType().FirstOrDefault() is { } textBlock) { // JSON is guaranteed to match the schema var contact = JsonSerializer.Deserialize>(textBlock.Text)!; Console.WriteLine($"{contact["name"]} ({contact["email"]})"); } ``` ```go Go import ( // ... "github.com/anthropics/anthropic-sdk-go" "github.com/invopop/jsonschema" ) type ContactInfo struct { Name string `json:"name" jsonschema:"description=Full name"` Email string `json:"email" jsonschema:"description=Email address"` PlanInterest string `json:"plan_interest" jsonschema:"description=Plan type"` DemoRequested bool `json:"demo_requested" jsonschema:"description=Whether a demo was requested"` } func generateSchema(v any) map[string]any { r := jsonschema.Reflector{AllowAdditionalProperties: false, DoNotReference: true} s := r.Reflect(v) b, _ := json.Marshal(s) var m map[string]any json.Unmarshal(b, &m) return m } // ... schema := generateSchema(&ContactInfo{}) message, _ := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock( "Extract the key information from this email: John Smith (john@example.com) is interested in our Enterprise plan and wants to schedule a demo for next Tuesday at 2pm.", )), }, OutputConfig: anthropic.OutputConfigParam{ Format: anthropic.JSONOutputFormatParam{ Schema: schema, }, }, }) for _, block := range message.Content { switch variant := block.AsAny().(type) { case anthropic.TextBlock: var contact ContactInfo json.Unmarshal([]byte(variant.Text), &contact) fmt.Printf("%s (%s)\n", contact.Name, contact.Email) } } ``` ```java Java static class ContactInfo { public String name; public String email; public String planInterest; public boolean demoRequested; } void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); StructuredMessageCreateParams createParams = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .outputConfig(ContactInfo.class) .addUserMessage("Extract the key information from this email: John Smith (john@example.com) is interested in our Enterprise plan and wants to schedule a demo for next Tuesday at 2pm.") .build(); StructuredMessage response = client.messages().create(createParams); ContactInfo contact = response.content().stream() .flatMap(block -> block.text().stream()) .findFirst().orElseThrow().text(); IO.println(contact.name + " (" + contact.email + ")"); } ``` ```php PHP use Anthropic\Lib\Concerns\StructuredOutputModelTrait; use Anthropic\Lib\Contracts\StructuredOutputModel; $client = new Client(); class ContactInfo implements StructuredOutputModel { use StructuredOutputModelTrait; public string $name; public string $email; public string $plan_interest; public bool $demo_requested; } $message = $client->messages->create( maxTokens: 1024, messages: [ ['role' => 'user', 'content' => 'Extract the key information from this email: John Smith (john@example.com) is interested in our Enterprise plan and wants to schedule a demo for next Tuesday at 2pm.'], ], model: 'claude-opus-5', outputConfig: ['format' => ContactInfo::class], ); $contact = $message->parsedOutput(); if ($contact instanceof ContactInfo) { echo "{$contact->name} ({$contact->email})\n"; } ``` ```ruby Ruby client = Anthropic::Client.new class ContactInfo < Anthropic::BaseModel required :name, String required :email, String required :plan_interest, String required :demo_requested, Anthropic::Boolean end message = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [{ role: "user", content: "Extract the key information from this email: John Smith (john@example.com) is interested in our Enterprise plan and wants to schedule a demo for next Tuesday at 2pm." }], output_config: {format: ContactInfo} ) contact = message.parsed_output puts "#{contact.name} (#{contact.email})" ``` #### SDK-specific methods Each SDK provides helpers that make working with structured outputs easier. See individual SDK pages for full details. **Raw JSON schemas through heredoc body** The CLI passes raw JSON schemas as a YAML heredoc body. Use the GJSON `@fromstr` modifier with `--transform` to parse the JSON string returned in the text content block and project specific fields. ```bash ant messages create \ --transform 'content.#(type=="text").text|@fromstr|{name,email}' \ --format yaml <<'YAML' model: claude-opus-5 max_tokens: 1024 messages: - role: user content: >- Extract contact info: John Smith, john@example.com, interested in the Pro plan output_config: format: type: json_schema schema: type: object properties: name: {type: string} email: {type: string} plan_interest: {type: string} required: [name, email, plan_interest] additionalProperties: false YAML ``` ```yaml Output name: John Smith email: john@example.com ``` **`client.messages.parse()` (Recommended)** The `parse()` method automatically transforms your Pydantic model, validates the response, and returns a `parsed_output` attribute. ```python from pydantic import BaseModel # ... class ContactInfo(BaseModel): name: str email: str plan_interest: str # ... response = client.messages.parse( model="claude-opus-5", max_tokens=1024, messages=[ { "role": "user", "content": "Extract contact info: John Smith, john@example.com, interested in the Pro plan", } ], output_format=ContactInfo, ) # Access the parsed output directly contact = response.parsed_output print(contact.name, contact.email) ``` **`transform_schema()` helper** For when you need to manually transform schemas before sending, or when you want to modify a Pydantic-generated schema. Unlike `client.messages.parse()`, which transforms provided schemas automatically, this gives you the transformed schema so you can further customize it. ```python from anthropic import transform_schema from pydantic import TypeAdapter # ... # First convert Pydantic model to JSON schema, then transform schema = TypeAdapter(ContactInfo).json_schema() schema = transform_schema(schema) # Modify schema if needed schema["properties"]["custom_field"] = {"type": "string"} response = client.messages.create( model="claude-opus-5", max_tokens=1024, messages=[{"role": "user", "content": "..."}], output_config={ "format": {"type": "json_schema", "schema": schema}, }, ) ``` **`client.messages.parse()` with `zodOutputFormat()`** The `parse()` method accepts a Zod schema, validates the response, and returns a `parsed_output` attribute with the inferred TypeScript type matching the schema. ```typescript import { z } from "zod"; import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod"; const ContactInfo = z.object({ name: z.string(), email: z.string(), planInterest: z.string() }); const client = new Anthropic(); const response = await client.messages.parse({ model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: "Extract contact info: John Smith, john@example.com, interested in the Pro plan" } ], output_config: { format: zodOutputFormat(ContactInfo) } }); // Guaranteed type-safe console.log(response.parsed_output!.email); ``` **`client.messages.parse()` with `jsonSchemaOutputFormat()`** The `jsonSchemaOutputFormat()` helper accepts a JSON Schema object and integrates it with `parse()` without requiring Zod. Zod is an optional peer dependency you install separately; `jsonSchemaOutputFormat()` works out of the box because the SDK bundles `json-schema-to-ts` directly. For **inline schema literals** (declared with `as const` in your source), you also get compile-time type inference: `parsed_output` is typed to match the schema structure. For **imported or generated schemas** (from a JSON file or OpenAPI codegen), the helper still sends the schema and parses the response, but the inferred type is `unknown` because `as const` can only apply to literal expressions. ```typescript import { jsonSchemaOutputFormat } from "@anthropic-ai/sdk/helpers/json-schema"; const client = new Anthropic(); const response = await client.messages.parse({ model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: "Extract contact info: John Smith, john@example.com, interested in the Pro plan" } ], output_config: { format: jsonSchemaOutputFormat({ type: "object", properties: { name: { type: "string" }, email: { type: "string" }, planInterest: { type: "string" } }, required: ["name", "email", "planInterest"], additionalProperties: false } as const) } }); // response.parsed_output is typed as { name: string; email: string; planInterest: string } | null console.log(response.parsed_output!.email); ``` **Type inference requires `as const`.** Use a literal object expression with a `const` assertion so TypeScript can narrow the property types. Without `as const`, the inferred type collapses to `unknown`. **Schema transformation.** By default, the helper transforms the schema the same way `zodOutputFormat()` does: removing unsupported constraints, adding `additionalProperties: false` to objects, and filtering string formats. Pass `jsonSchemaOutputFormat(schema, { transform: false })` to send your schema to the API unchanged. See [How SDK transformation works](https://platform.claude.com/docs/en/build-with-claude/structured-outputs#how-sdk-transformation-works). **JSON schemas through `OutputConfig`** The C# SDK accepts raw JSON schemas built programmatically with `JsonSerializer.SerializeToElement`, as shown here, or derives the schema from a plain C# class with the generic `Create()` overload. Deserialize the response JSON with `JsonSerializer.Deserialize`. ```csharp using System.Text.Json; using Anthropic; using Anthropic.Models.Messages; var client = new AnthropicClient(); var response = await client.Messages.Create(new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [new() { Role = Role.User, Content = "Extract the key information from this email: John Smith (john@example.com) is interested in our Enterprise plan." }], OutputConfig = new OutputConfig { Format = new JsonOutputFormat { Schema = new Dictionary { ["type"] = JsonSerializer.SerializeToElement("object"), ["properties"] = JsonSerializer.SerializeToElement(new { name = new { type = "string" }, email = new { type = "string" }, plan_interest = new { type = "string" }, }), ["required"] = JsonSerializer.SerializeToElement( new[] { "name", "email", "plan_interest" }), ["additionalProperties"] = JsonSerializer.SerializeToElement(false), }, }, }, }); if (response.Content.Select(b => b.Value).OfType().FirstOrDefault() is { } textBlock) { // JSON is guaranteed to match the schema var contact = JsonSerializer.Deserialize>(textBlock.Text)!; Console.WriteLine($"{contact["name"]} ({contact["email"]})"); } ``` **Raw JSON schemas through `OutputConfigParam`** The Go SDK works with raw JSON schemas. Define a Go struct with json tags, generate the JSON schema (for example, using `invopop/jsonschema`), and unmarshal the response text into your struct. On the beta API, passing a struct as the output format schema reflects it into a JSON schema automatically. ```go import ( // ... "github.com/anthropics/anthropic-sdk-go" "github.com/invopop/jsonschema" ) type ContactInfo struct { Name string `json:"name" jsonschema:"description=Full name"` Email string `json:"email" jsonschema:"description=Email address"` PlanInterest string `json:"plan_interest" jsonschema:"description=Plan type"` } func generateSchema(v any) map[string]any { r := jsonschema.Reflector{AllowAdditionalProperties: false, DoNotReference: true} s := r.Reflect(v) b, _ := json.Marshal(s) var m map[string]any json.Unmarshal(b, &m) return m } // ... schema := generateSchema(&ContactInfo{}) message, _ := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock( "Extract the key information from this email: John Smith (john@example.com) is interested in our Enterprise plan.", )), }, OutputConfig: anthropic.OutputConfigParam{ Format: anthropic.JSONOutputFormatParam{ Schema: schema, }, }, }) for _, block := range message.Content { switch variant := block.AsAny().(type) { case anthropic.TextBlock: var contact ContactInfo json.Unmarshal([]byte(variant.Text), &contact) fmt.Printf("%s (%s)\n", contact.Name, contact.Email) } } ``` Java examples on this page use [JDK 25 compact source file](https://openjdk.org/jeps/512) syntax; see the [Java SDK requirements](https://platform.claude.com/docs/en/cli-sdks-libraries/sdks/java#requirements) for the substitution on earlier JDKs. **`outputConfig(Class)` method** Pass a Java class to `outputConfig()` and the SDK automatically derives a JSON schema, validates it, and returns a `StructuredMessageCreateParams`. Access the parsed result through `response.content().stream().flatMap(block -> block.text().stream()).findFirst().orElseThrow().text()`. Declare your schema classes as top-level classes or `static` nested classes. This requirement comes from the Jackson Databind library (`com.fasterxml.jackson.databind`), which the SDK uses to deserialize JSON responses into your class instances and cannot instantiate non-static inner classes. ```java static class ContactInfo { public String name; public String email; public String planInterest; } void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); StructuredMessageCreateParams createParams = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .outputConfig(ContactInfo.class) .addUserMessage("Extract contact info: John Smith, john@example.com, interested in the Pro plan") .build(); StructuredMessage response = client.messages().create(createParams); ContactInfo contact = response.content().stream() .flatMap(block -> block.text().stream()) .findFirst().orElseThrow().text(); IO.println(contact.name + " (" + contact.email + ")"); } ``` Java retains generic type information for fields in the class's metadata, but generic type erasure applies in other scopes. While a JSON schema can be derived from a `BookList.books` field with type `List`, a valid JSON schema cannot be derived from a local variable of that same type. If an error occurs while converting a JSON response to a Java class instance, the error message includes the JSON response to assist in diagnosis. If your JSON response may contain sensitive information, avoid logging it directly, or ensure that you redact any sensitive details from the error message. Structured outputs support a [subset of the JSON Schema language](https://platform.claude.com/docs/en/build-with-claude/structured-outputs#json-schema-limitations). The SDK generates schemas automatically from classes to align with this subset. The `outputConfig(Class)` method performs a validation check on the schema derived from the specified class. Key points: * **Local validation** occurs without sending requests to the remote AI model. * **Remote validation** is also performed by the AI model upon receiving the JSON schema. * **Version compatibility:** Local validation may fail while remote validation succeeds if the SDK version is outdated. * **Disabling local validation:** Pass `JsonSchemaLocalValidation.NO` if you encounter compatibility issues: ```java import com.anthropic.core.JsonSchemaLocalValidation; // ... static class BookList { public List books; } void main() { StructuredMessageCreateParams createParams = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(2048) .outputConfig(BookList.class, JsonSchemaLocalValidation.NO) .addUserMessage("List some famous late twentieth century novels.") .build(); } ``` Structured outputs also work with streaming. As responses arrive in stream events, you need to accumulate the full response before deserializing the JSON. Use `MessageAccumulator` to collect the JSON strings from the stream. Once accumulated, call `MessageAccumulator.message(Class)` to convert the accumulated `Message` into a `StructuredMessage`, which automatically deserializes the JSON into your Java class. When the SDK derives a JSON schema from your Java classes, it includes all properties represented by `public` fields or `public` getter methods by default and excludes non-`public` fields and getter methods. You can control visibility with annotations: * `@JsonIgnore` excludes a `public` field or getter method * `@JsonProperty` includes a non-`public` field or getter method If you define `private` fields with `public` getter methods, the SDK derives the property name from the getter (for example, `private` field `myValue` with `public` method `getMyValue()` produces a `"myValue"` property). To use a non-conventional getter name, annotate the method with `@JsonProperty`. Each class must define at least one property for the JSON schema. A validation error occurs if no fields or getter methods can produce schema properties, such as when: * There are no fields or getter methods in the class * All `public` members are annotated with `@JsonIgnore` * All non-`public` members lack `@JsonProperty` annotations * A field uses a `Map` type, which produces an empty `"properties"` field Your Java classes can use composition and inheritance to share structure when defining JSON schemas. Each pattern affects the output structure differently. **Composition** produces nested JSON output. Deriving a schema from class `Composed` that composes `A` and `B`: ```java static class A { public String a; } static class B { public String b; } static class Composed { public A composedA; public B composedB; } ``` The JSON output has this nested structure: ```json { "composedA": { "a": "hello" }, "composedB": { "b": "world" } } ``` **Inheritance** produces flat JSON output. Deriving a schema from class `Derived` that extends `Base`: ```java static class Base { public String a; } static class Derived extends Base { public String b; } ``` The JSON output has this flat structure: ```json { "a": "hello", "b": "world" } ``` You can use Jackson Databind annotations to enrich the JSON schema derived from your Java classes: ```java import com.fasterxml.jackson.annotation.JsonClassDescription; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonPropertyDescription; static class Person { @JsonPropertyDescription("The first name and surname of the person") public String name; public int birthYear; @JsonPropertyDescription("The year the person died, or 'present' if the person is living.") public String deathYear; } @JsonClassDescription("The details of one published book") static class Book { public String title; public Person author; @JsonPropertyDescription("The year in which the book was first published.") public int publicationYear; @JsonIgnore public String genre; } static class BookList { public List books; } ``` Annotation summary: * `@JsonClassDescription`: Add a description to a class * `@JsonPropertyDescription`: Add a description to a field or getter method * `@JsonIgnore`: Exclude a `public` field or getter from the schema * `@JsonProperty`: Include a non-`public` field or getter in the schema If you use `@JsonProperty(required = false)`, the SDK ignores the `false` value. Class-derived schemas always mark all properties as required. You can also use Swagger Core (OpenAPI 3) `@Schema` and `@ArraySchema` annotations for type-specific constraints: ```java import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; static class Article { @ArraySchema(minItems = 1) public List authors; public String title; @Schema(format = "date") public String publicationDate; public int pageCount; } ``` Local validation checks that you haven't used any unsupported constraint keywords, but constraint values aren't validated locally. For example, an unsupported `"format"` value may pass local validation but cause a remote error. If you use both Jackson and Swagger annotations to set the same schema field, the Jackson annotation takes precedence. Class-based schema derivation is the most convenient path, but for direct control over the schema structure you can build a `JsonOutputFormat.Schema` manually and wrap it in an `OutputConfig`. ```java import com.anthropic.core.JsonValue; import com.anthropic.models.messages.JsonOutputFormat; // ... import com.anthropic.models.messages.OutputConfig; void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); JsonOutputFormat.Schema schema = JsonOutputFormat.Schema.builder() .putAdditionalProperty("type", JsonValue.from("object")) .putAdditionalProperty("properties", JsonValue.from(Map.of( "name", Map.of("type", "string"), "email", Map.of("type", "string"), "plan_interest", Map.of("type", "string")))) .putAdditionalProperty("required", JsonValue.from( List.of("name", "email", "plan_interest"))) .putAdditionalProperty("additionalProperties", JsonValue.from(false)) .build(); OutputConfig outputConfig = OutputConfig.builder() .format(JsonOutputFormat.builder().schema(schema).build()) .build(); MessageCreateParams createParams = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .outputConfig(outputConfig) .addUserMessage( "John Smith (john@example.com) is interested in our Enterprise plan.") .build(); client.messages().create(createParams).content().stream() .flatMap(contentBlock -> contentBlock.text().stream()) .forEach(textBlock -> IO.println(textBlock.text())); } ``` For a more extensive example that builds a nested schema with arrays and descriptions, see [`StructuredOutputsRawExample.java`](https://github.com/anthropics/anthropic-sdk-java/blob/main/anthropic-java-example/src/main/java/com/anthropic/example/StructuredOutputsRawExample.java) in the SDK repository. **Classes through the `StructuredOutputModel` interface** Define a PHP class implementing `StructuredOutputModel` (using `StructuredOutputModelTrait`) and pass the class name to `outputConfig: ['format' => MyClass::class]`. The SDK derives a JSON schema from your native PHP 8 property types and returns a typed instance through `$message->parsedOutput()`. `parsedOutput()` returns your model instance on success, or `null` (or an error array) if parsing fails. Use `instanceof` to narrow the type before accessing fields. ```php use Anthropic\Lib\Concerns\StructuredOutputModelTrait; use Anthropic\Lib\Contracts\StructuredOutputModel; $client = new Client(); class ContactInfo implements StructuredOutputModel { use StructuredOutputModelTrait; public string $name; public string $email; public string $plan_interest; } $message = $client->messages->create( maxTokens: 1024, messages: [ ['role' => 'user', 'content' => 'Extract the key information from this email: John Smith (john@example.com) is interested in our Enterprise plan.'], ], model: 'claude-opus-5', outputConfig: ['format' => ContactInfo::class], ); $contact = $message->parsedOutput(); if ($contact instanceof ContactInfo) { echo "{$contact->name} ({$contact->email})\n"; } ``` The SDK maps native PHP 8 property types to JSON Schema: | PHP type | JSON Schema | | ------------------------------------------ | ---------------------------------- | | `string` | `"string"` | | `int` | `"integer"` | | `float` | `"number"` | | `bool` | `"boolean"` | | `array` | `"array"` (see the following note) | | `?type` (nullable) | Optional field | | Class implementing `StructuredOutputModel` | Nested object | For `array` properties, the SDK adds an `items` schema only when the element type is a nested `StructuredOutputModel`, declared with `#[Constrained(itemClass: MyModel::class)]` or a `/** @var MyModel[] */` docblock. Arrays of scalars (`string[]`, `int[]`) emit an unconstrained `{"type":"array"}`. All non-nullable properties become required fields. Add constraints with the `#[Constrained]` attribute: ```php use Anthropic\Lib\Attributes\Constrained; use Anthropic\Lib\Concerns\StructuredOutputModelTrait; use Anthropic\Lib\Contracts\StructuredOutputModel; class Address implements StructuredOutputModel { use StructuredOutputModelTrait; public string $street; } class Profile implements StructuredOutputModel { use StructuredOutputModelTrait; #[Constrained(description: 'Age in years', minimum: 0, maximum: 150)] public int $age; #[Constrained(format: 'email')] public string $email; #[Constrained(itemClass: Address::class, minItems: 1)] public array $addresses; } ``` **API-enforced constraints** (sent in the schema): `description`, `format`, `const`, `itemClass`, `minItems` (0 or 1 only). **SDK-validated constraints** (stripped from the wire schema, appended to the description, and validated against the response): `minimum`, `maximum`, `multipleOf`, `minLength`, `maxLength`. For schemas that PHP type hints can't express, pass a raw associative array through `OutputConfig::with()`. This path skips the `parsedOutput()` helper; decode the response with `json_decode()`: ```php use Anthropic\Messages\OutputConfig; use Anthropic\Messages\JSONOutputFormat; $client = new Client(); $message = $client->messages->create( maxTokens: 1024, messages: [ ['role' => 'user', 'content' => 'Extract the key information from this email: John Smith (john@example.com) is interested in our Enterprise plan.'], ], model: 'claude-opus-5', outputConfig: OutputConfig::with(format: JSONOutputFormat::with(schema: [ 'type' => 'object', 'properties' => [ 'name' => ['type' => 'string'], 'email' => ['type' => 'string'], 'plan_interest' => ['type' => 'string'], ], 'required' => ['name', 'email', 'plan_interest'], 'additionalProperties' => false, ])), ); $textBlock = array_find($message->content, static fn ($block): bool => $block->type === 'text'); $contact = json_decode($textBlock->text, associative: true); echo "{$contact['name']} ({$contact['email']})\n"; ``` **`output_config: {format: Model}` with `parsed_output`** Define a model class extending `Anthropic::BaseModel` and pass it as the format to `messages.create()`. The response includes a `parsed_output` attribute with a typed Ruby object. ```ruby class ContactInfo < Anthropic::BaseModel required :name, String required :email, String required :plan_interest, String end client = Anthropic::Client.new message = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: "Extract contact info: John Smith, john@example.com, interested in the Pro plan" } ], output_config: {format: ContactInfo} ) contact = message.parsed_output puts "#{contact.name} (#{contact.email})" ``` The Ruby SDK supports additional model definition features for richer schemas: * **`doc:` keyword:** Add descriptions to fields for more informative schema output * **`Anthropic::ArrayOf[T]`:** Typed arrays. Pass array-level constraints (`min_items:`, `max_items:`) as keywords on `required`/`optional`, not on `ArrayOf` itself * **`Anthropic::EnumOf[:a, :b]`:** Enum fields with constrained values * **`Anthropic::UnionOf[T1, T2]`:** Union types mapped to `anyOf` ```ruby class FamousNumber < Anthropic::BaseModel required :value, Float optional :reason, String, doc: "why is this number mathematically significant?" end class Output < Anthropic::BaseModel required :numbers, Anthropic::ArrayOf[FamousNumber], min_items: 3, max_items: 5 end message = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [{role: "user", content: "give me some famous numbers"}], output_config: {format: Output} ) message.parsed_output # => #...]> ``` #### How SDK transformation works The Python, TypeScript, Ruby, and PHP SDKs automatically transform schemas with unsupported features. The C# and Go SDKs apply the same transformations when the schema is derived from a native type (`Create()` in C#; struct reflection or `BetaJSONSchemaOutputFormat()` on the Go beta API). The transformation steps: 1. **Remove unsupported constraints** (for example, `minimum`, `maximum`, `minLength`, `maxLength`) 2. **Update descriptions** with constraint info (for example, "Must be at least 100"), when the constraint is not directly supported with structured outputs 3. **Add `additionalProperties: false`** to all objects 4. **Filter string formats** to supported list only 5. **Validate responses** against your original schema (with all constraints) This means Claude receives a simplified schema, but your code still enforces all constraints through validation. **Example:** A Pydantic field with `minimum: 100` becomes a plain integer in the sent schema, but the SDK updates the description to "Must be at least 100" and validates the response against the original constraint. ### Common use cases Extract structured data from unstructured text: ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-opus-4-8", "max_tokens": 4096, "messages": [ { "role": "user", "content": "Extract invoice data from: Invoice #12345, Date: 2024-01-15, Total: $500.00" } ], "output_config": { "format": { "type": "json_schema", "schema": { "type": "object", "properties": { "invoice_number": {"type": "string"}, "date": {"type": "string"}, "total_amount": {"type": "number"}, "line_items": { "type": "array", "items": {"type": "object", "additionalProperties": false} }, "customer_name": {"type": "string"} }, "required": ["invoice_number", "date", "total_amount", "line_items", "customer_name"], "additionalProperties": false } } } }' ``` ```bash CLI ant messages create \ --transform 'content.#(type=="text").text|@fromstr' \ --format jsonl <<'YAML' model: claude-opus-5 max_tokens: 4096 messages: - role: user content: "Extract invoice data from: Invoice #12345, Date: 2024-01-15, Total: $500.00" output_config: format: type: json_schema schema: type: object properties: invoice_number: {type: string} date: {type: string} total_amount: {type: number} line_items: type: array items: {type: object, additionalProperties: false} customer_name: {type: string} required: [invoice_number, date, total_amount, line_items, customer_name] additionalProperties: false YAML ``` ```python Python from pydantic import BaseModel class Invoice(BaseModel): invoice_number: str date: str total_amount: float line_items: list[dict] customer_name: str client = anthropic.Anthropic() invoice_text = "Invoice #12345, Date: 2024-01-15, Total: $500.00" response = client.messages.parse( model="claude-opus-5", max_tokens=4096, output_format=Invoice, messages=[ {"role": "user", "content": f"Extract invoice data from: {invoice_text}"} ], ) print(response.parsed_output) ``` ```typescript TypeScript import { z } from "zod"; import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod"; const client = new Anthropic(); const InvoiceSchema = z.object({ invoice_number: z.string(), date: z.string(), total_amount: z.number(), line_items: z.array(z.record(z.string(), z.any())), customer_name: z.string() }); const invoiceText = "Invoice #12345, Date: 2024-01-15, Total: $500.00"; const response = await client.messages.parse({ model: "claude-opus-5", max_tokens: 4096, output_config: { format: zodOutputFormat(InvoiceSchema) }, messages: [{ role: "user", content: `Extract invoice data from: ${invoiceText}` }] }); console.log(response.parsed_output); ``` ```csharp C# AnthropicClient client = new(); string invoiceText = "Invoice #12345, Date: 2024-01-15, Total: $500.00"; var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 4096, OutputConfig = new OutputConfig { Format = new JsonOutputFormat { Schema = new Dictionary { ["type"] = JsonSerializer.SerializeToElement("object"), ["properties"] = JsonSerializer.SerializeToElement(new { invoice_number = new { type = "string" }, date = new { type = "string" }, total_amount = new { type = "number" }, line_items = new { type = "array", items = new { type = "object", additionalProperties = false, }, }, customer_name = new { type = "string" }, }), ["required"] = JsonSerializer.SerializeToElement(new[] { "invoice_number", "date", "total_amount", "line_items", "customer_name" }), ["additionalProperties"] = JsonSerializer.SerializeToElement(false), }, }, }, Messages = [new() { Role = Role.User, Content = $"Extract invoice data from: {invoiceText}" }] }; var message = await client.Messages.Create(parameters); Console.WriteLine(message); ``` ```go Go client := anthropic.NewClient() invoiceText := "Invoice #12345, Date: 2024-01-15, Total: $500.00" schema := map[string]any{ "type": "object", "additionalProperties": false, "properties": map[string]any{ "invoice_number": map[string]any{"type": "string"}, "date": map[string]any{"type": "string"}, "total_amount": map[string]any{"type": "number"}, "line_items": map[string]any{ "type": "array", "items": map[string]any{ "type": "object", "additionalProperties": false, "properties": map[string]any{ "description": map[string]any{"type": "string"}, "quantity": map[string]any{"type": "number"}, "unit_price": map[string]any{"type": "number"}, }, "required": []string{"description", "quantity", "unit_price"}, }, }, "customer_name": map[string]any{"type": "string"}, }, "required": []string{"invoice_number", "date", "total_amount", "line_items", "customer_name"}, } response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 4096, OutputConfig: anthropic.OutputConfigParam{ Format: anthropic.JSONOutputFormatParam{ Schema: schema, }, }, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock(fmt.Sprintf("Extract invoice data from: %s", invoiceText))), }, }) if err != nil { log.Fatal(err) } for _, block := range response.Content { switch variant := block.AsAny().(type) { case anthropic.TextBlock: fmt.Println(variant.Text) } } ``` ```java Java import com.fasterxml.jackson.annotation.JsonProperty; static class LineItem { @JsonProperty("description") public String description; @JsonProperty("quantity") public int quantity; @JsonProperty("unit_price") public double unitPrice; } static class Invoice { @JsonProperty("invoice_number") public String invoiceNumber; @JsonProperty("date") public String date; @JsonProperty("total_amount") public double totalAmount; @JsonProperty("line_items") public List lineItems; @JsonProperty("customer_name") public String customerName; } void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); String invoiceText = "Invoice #12345, Date: 2024-01-15, Total: $500.00"; StructuredMessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(4096L) .outputConfig(Invoice.class) .addUserMessage("Extract invoice data from: " + invoiceText) .build(); StructuredMessage response = client.messages().create(params); Invoice invoice = response.content().stream() .flatMap(block -> block.text().stream()) .findFirst().orElseThrow().text(); IO.println(invoice.invoiceNumber + ": $" + invoice.totalAmount); } ``` ```php PHP use Anthropic\Lib\Concerns\StructuredOutputModelTrait; use Anthropic\Lib\Contracts\StructuredOutputModel; $client = new Client(); class Invoice implements StructuredOutputModel { use StructuredOutputModelTrait; public string $invoice_number; public string $date; public float $total_amount; public array $line_items; public string $customer_name; } $invoiceText = "Invoice #12345, Date: 2024-01-15, Total: $500.00"; $message = $client->messages->create( maxTokens: 4096, messages: [ ['role' => 'user', 'content' => "Extract invoice data from: $invoiceText"] ], model: 'claude-opus-5', outputConfig: ['format' => Invoice::class], ); $invoice = $message->parsedOutput(); if ($invoice instanceof Invoice) { echo "Invoice {$invoice->invoice_number}: \${$invoice->total_amount}\n"; } ``` ```ruby Ruby client = Anthropic::Client.new class LineItem < Anthropic::BaseModel required :description, String required :amount, Float end class Invoice < Anthropic::BaseModel required :invoice_number, String required :date, String required :total_amount, Float required :line_items, Anthropic::ArrayOf[LineItem] required :customer_name, String end invoice_text = "Invoice #12345, Date: 2024-01-15, Total: $500.00" message = client.messages.create( model: "claude-opus-5", max_tokens: 4096, output_config: {format: Invoice}, messages: [ {role: "user", content: "Extract invoice data from: #{invoice_text}"} ] ) invoice = message.parsed_output puts "Invoice #{invoice.invoice_number}: $#{invoice.total_amount}" ``` Classify content with structured categories: ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-opus-4-8", "max_tokens": 1024, "messages": [ { "role": "user", "content": "Classify this feedback: Great product, fast shipping!" } ], "output_config": { "format": { "type": "json_schema", "schema": { "type": "object", "properties": { "category": {"type": "string"}, "confidence": {"type": "number"}, "tags": {"type": "array", "items": {"type": "string"}}, "sentiment": {"type": "string"} }, "required": ["category", "confidence", "tags", "sentiment"], "additionalProperties": false } } } }' ``` ```bash CLI ant messages create \ --transform 'content.#(type=="text").text|@fromstr' \ --format jsonl <<'YAML' model: claude-opus-5 max_tokens: 1024 messages: - role: user content: "Classify this feedback: Great product, fast shipping!" output_config: format: type: json_schema schema: type: object properties: category: type: string confidence: type: number tags: type: array items: type: string sentiment: type: string required: - category - confidence - tags - sentiment additionalProperties: false YAML ``` ```python Python from pydantic import BaseModel client = Anthropic() class Classification(BaseModel): category: str confidence: float tags: list[str] sentiment: str feedback_text = "Great product, but the delivery was slow." response = client.messages.parse( model="claude-opus-5", max_tokens=1024, output_format=Classification, messages=[{"role": "user", "content": f"Classify this feedback: {feedback_text}"}], ) print(response.parsed_output) ``` ```typescript TypeScript import { z } from "zod"; import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod"; const client = new Anthropic(); const ClassificationSchema = z.object({ category: z.string(), confidence: z.number(), tags: z.array(z.string()), sentiment: z.string() }); const feedbackText = "Great product, but the delivery was slow."; const response = await client.messages.parse({ model: "claude-opus-5", max_tokens: 1024, output_config: { format: zodOutputFormat(ClassificationSchema) }, messages: [{ role: "user", content: `Classify this feedback: ${feedbackText}` }] }); console.log(response.parsed_output); ``` ```csharp C# string feedbackText = "Great product, fast shipping!"; var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [new() { Role = Role.User, Content = $"Classify this feedback: {feedbackText}" }], OutputConfig = new OutputConfig { Format = new JsonOutputFormat { Schema = new Dictionary { ["type"] = JsonSerializer.SerializeToElement("object"), ["properties"] = JsonSerializer.SerializeToElement(new { category = new { type = "string" }, confidence = new { type = "number" }, tags = new { type = "array", items = new { type = "string" } }, sentiment = new { type = "string" }, }), ["required"] = JsonSerializer.SerializeToElement(new[] { "category", "confidence", "tags", "sentiment" }), ["additionalProperties"] = JsonSerializer.SerializeToElement(false), }, }, }, }; var message = await client.Messages.Create(parameters); Console.WriteLine(message); ``` ```go Go feedbackText := "Great product, fast shipping!" schema := map[string]any{ "type": "object", "properties": map[string]any{ "category": map[string]any{"type": "string"}, "confidence": map[string]any{"type": "number"}, "tags": map[string]any{"type": "array", "items": map[string]any{"type": "string"}}, "sentiment": map[string]any{"type": "string"}, }, "required": []string{"category", "confidence", "tags", "sentiment"}, "additionalProperties": false, } response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, OutputConfig: anthropic.OutputConfigParam{ Format: anthropic.JSONOutputFormatParam{ Schema: schema, }, }, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock(fmt.Sprintf("Classify this feedback: %s", feedbackText))), }, }) if err != nil { log.Fatal(err) } for _, block := range response.Content { switch variant := block.AsAny().(type) { case anthropic.TextBlock: var result map[string]any json.Unmarshal([]byte(variant.Text), &result) fmt.Println(result) } } ``` ```java Java import com.fasterxml.jackson.annotation.JsonProperty; static class Classification { @JsonProperty("category") public String category; @JsonProperty("confidence") public double confidence; @JsonProperty("tags") public List tags; @JsonProperty("sentiment") public String sentiment; } void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); String feedbackText = "Great product, fast shipping!"; StructuredMessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .outputConfig(Classification.class) .addUserMessage("Classify this feedback: " + feedbackText) .build(); StructuredMessage response = client.messages().create(params); Classification result = response.content().stream() .flatMap(block -> block.text().stream()) .findFirst().orElseThrow().text(); IO.println(result.category + " (" + result.confidence + ")"); } ``` ```php PHP use Anthropic\Lib\Concerns\StructuredOutputModelTrait; use Anthropic\Lib\Contracts\StructuredOutputModel; $client = new Client(); class Classification implements StructuredOutputModel { use StructuredOutputModelTrait; public string $category; public float $confidence; public array $tags; public string $sentiment; } $feedbackText = "Great product, fast shipping!"; $message = $client->messages->create( maxTokens: 1024, messages: [ ['role' => 'user', 'content' => "Classify this feedback: {$feedbackText}"] ], model: 'claude-opus-5', outputConfig: ['format' => Classification::class], ); $result = $message->parsedOutput(); if ($result instanceof Classification) { echo "{$result->category} ({$result->confidence}): {$result->sentiment}\n"; } ``` ```ruby Ruby client = Anthropic::Client.new class Classification < Anthropic::BaseModel required :category, String required :confidence, Float required :tags, Anthropic::ArrayOf[String] required :sentiment, String end feedback_text = "Great product, fast shipping!" message = client.messages.create( model: "claude-opus-5", max_tokens: 1024, output_config: {format: Classification}, messages: [ {role: "user", content: "Classify this feedback: #{feedback_text}"} ] ) puts message.parsed_output ``` Generate API-ready responses: ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-opus-4-8", "max_tokens": 1024, "messages": [ { "role": "user", "content": "Process this request: ..." } ], "output_config": { "format": { "type": "json_schema", "schema": { "type": "object", "properties": { "status": {"type": "string"}, "data": {"type": "object", "additionalProperties": false}, "errors": { "type": "array", "items": {"type": "object", "additionalProperties": false} }, "metadata": {"type": "object", "additionalProperties": false} }, "required": ["status", "data", "metadata"], "additionalProperties": false } } } }' ``` ```bash CLI ant messages create \ --transform 'content.#(type=="text").text' \ --raw-output <<'YAML' model: claude-opus-5 max_tokens: 1024 output_config: format: type: json_schema schema: type: object properties: status: type: string data: type: object additionalProperties: false errors: type: array items: type: object additionalProperties: false metadata: type: object additionalProperties: false required: - status - data - metadata additionalProperties: false messages: - role: user content: "Process this request: ..." YAML ``` ```python Python from pydantic import BaseModel client = Anthropic() class APIResponse(BaseModel): status: str data: dict errors: list[dict] | None metadata: dict response = client.messages.parse( model="claude-opus-5", max_tokens=1024, output_format=APIResponse, messages=[{"role": "user", "content": "Process this request: ..."}], ) print(response.parsed_output) ``` ```typescript TypeScript import { z } from "zod"; import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod"; const client = new Anthropic(); const APIResponseSchema = z.object({ status: z.string(), data: z.record(z.string(), z.any()), errors: z.array(z.record(z.string(), z.any())).optional(), metadata: z.record(z.string(), z.any()) }); const response = await client.messages.parse({ model: "claude-opus-5", max_tokens: 1024, output_config: { format: zodOutputFormat(APIResponseSchema) }, messages: [{ role: "user", content: "Process this request..." }] }); console.log(response.parsed_output); ``` ```csharp C# var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [new() { Role = Role.User, Content = "Process this request: ..." }], OutputConfig = new OutputConfig { Format = new JsonOutputFormat { Schema = new Dictionary { ["type"] = JsonSerializer.SerializeToElement("object"), ["properties"] = JsonSerializer.SerializeToElement(new { status = new { type = "string" }, data = new { type = "object", additionalProperties = false }, errors = new { type = "array", items = new { type = "object", additionalProperties = false }, }, metadata = new { type = "object", additionalProperties = false }, }), ["required"] = JsonSerializer.SerializeToElement(new[] { "status", "data", "metadata" }), ["additionalProperties"] = JsonSerializer.SerializeToElement(false), }, }, }, }; var message = await client.Messages.Create(parameters); Console.WriteLine(message); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, OutputConfig: anthropic.OutputConfigParam{ Format: anthropic.JSONOutputFormatParam{ Schema: map[string]any{ "type": "object", "additionalProperties": false, "properties": map[string]any{ "status": map[string]any{ "type": "string", }, "data": map[string]any{ "type": "object", "additionalProperties": false, }, "errors": map[string]any{ "type": "array", "items": map[string]any{ "type": "object", "additionalProperties": false, }, }, "metadata": map[string]any{ "type": "object", "additionalProperties": false, }, }, "required": []string{"status", "data", "metadata"}, }, }, }, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Process this request: ...")), }, }) if err != nil { log.Fatal(err) } for _, block := range response.Content { switch variant := block.AsAny().(type) { case anthropic.TextBlock: fmt.Println(variant.Text) } } ``` ```java Java import com.fasterxml.jackson.annotation.JsonProperty; static class APIData { @JsonProperty("message") public String message; @JsonProperty("resource_id") public String resourceId; } static class APIError { @JsonProperty("code") public String code; @JsonProperty("message") public String message; } static class APIMetadata { @JsonProperty("request_id") public String requestId; @JsonProperty("timestamp") public String timestamp; } static class APIResponse { @JsonProperty("status") public String status; @JsonProperty("data") public APIData data; @JsonProperty("errors") public List errors; @JsonProperty("metadata") public APIMetadata metadata; } void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); StructuredMessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .outputConfig(APIResponse.class) .addUserMessage("Process this request: ...") .build(); StructuredMessage response = client.messages().create(params); APIResponse result = response.content().stream() .flatMap(block -> block.text().stream()) .findFirst().orElseThrow().text(); IO.println(result.status); } ``` ```php PHP use Anthropic\Lib\Attributes\Constrained; use Anthropic\Lib\Concerns\StructuredOutputModelTrait; use Anthropic\Lib\Contracts\StructuredOutputModel; $client = new Client(); class Payload implements StructuredOutputModel { use StructuredOutputModelTrait; public string $message; } class APIError implements StructuredOutputModel { use StructuredOutputModelTrait; public string $code; public string $detail; } class Metadata implements StructuredOutputModel { use StructuredOutputModelTrait; public string $request_id; } class APIResponse implements StructuredOutputModel { use StructuredOutputModelTrait; public string $status; public Payload $data; #[Constrained(itemClass: APIError::class)] public ?array $errors; public Metadata $metadata; } $message = $client->messages->create( maxTokens: 1024, messages: [ ['role' => 'user', 'content' => 'Process this request: ...'] ], model: 'claude-opus-5', outputConfig: ['format' => APIResponse::class], ); $result = $message->parsedOutput(); if ($result instanceof APIResponse) { echo "{$result->status}: {$result->data->message}\n"; } ``` ```ruby Ruby client = Anthropic::Client.new class Payload < Anthropic::BaseModel required :message, String end class APIError < Anthropic::BaseModel required :code, String required :detail, String end class Metadata < Anthropic::BaseModel required :request_id, String end class APIResponse < Anthropic::BaseModel required :status, String required :data, Payload optional :errors, Anthropic::ArrayOf[APIError] required :metadata, Metadata end message = client.messages.create( model: "claude-opus-5", max_tokens: 1024, output_config: {format: APIResponse}, messages: [ {role: "user", content: "Process this request: ..."} ] ) puts message.parsed_output ``` ## Strict tool use For enforcing JSON Schema compliance on tool inputs with grammar-constrained sampling, see [Strict tool use](https://platform.claude.com/docs/en/agents-and-tools/tool-use/strict-tool-use). ## Using both features together JSON outputs and strict tool use solve different problems and work together: * **JSON outputs** control Claude's response format (what Claude says) * **Strict tool use** validates tool parameters (how Claude calls your functions) When combined, Claude can call tools with guaranteed-valid parameters AND return structured JSON responses. This is useful for agentic workflows where you need both reliable tool calls and structured final outputs. ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-opus-4-8", "max_tokens": 1024, "messages": [ { "role": "user", "content": "Help me plan a trip to Paris departing May 15, 2026" } ], "output_config": { "format": { "type": "json_schema", "schema": { "type": "object", "properties": { "summary": {"type": "string"}, "next_steps": {"type": "array", "items": {"type": "string"}} }, "required": ["summary", "next_steps"], "additionalProperties": false } } }, "tools": [ { "name": "search_flights", "strict": true, "input_schema": { "type": "object", "properties": { "destination": {"type": "string"}, "date": {"type": "string", "format": "date"} }, "required": ["destination", "date"], "additionalProperties": false } } ] }' ``` ```bash CLI ant messages create <<'YAML' model: claude-opus-5 max_tokens: 1024 messages: - role: user content: Help me plan a trip to Paris departing May 15, 2026 # JSON outputs: structured response format output_config: format: type: json_schema schema: type: object properties: summary: type: string next_steps: type: array items: type: string required: [summary, next_steps] additionalProperties: false # Strict tool use: guaranteed tool parameters tools: - name: search_flights strict: true input_schema: type: object properties: destination: type: string date: type: string format: date required: [destination, date] additionalProperties: false YAML ``` ```python Python response = client.messages.create( model="claude-opus-5", max_tokens=1024, messages=[ { "role": "user", "content": "Help me plan a trip to Paris departing May 15, 2026", } ], # JSON outputs: structured response format output_config={ "format": { "type": "json_schema", "schema": { "type": "object", "properties": { "summary": {"type": "string"}, "next_steps": {"type": "array", "items": {"type": "string"}}, }, "required": ["summary", "next_steps"], "additionalProperties": False, }, } }, # Strict tool use: guaranteed tool parameters tools=[ { "name": "search_flights", "strict": True, "input_schema": { "type": "object", "properties": { "destination": {"type": "string"}, "date": {"type": "string", "format": "date"}, }, "required": ["destination", "date"], "additionalProperties": False, }, } ], ) print(response) ``` ```typescript TypeScript const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, messages: [{ role: "user", content: "Help me plan a trip to Paris departing May 15, 2026" }], // JSON outputs: structured response format output_config: { format: { type: "json_schema", schema: { type: "object", properties: { summary: { type: "string" }, next_steps: { type: "array", items: { type: "string" } } }, required: ["summary", "next_steps"], additionalProperties: false } } }, // Strict tool use: guaranteed tool parameters tools: [ { name: "search_flights", description: "Search for available flights to a destination on a specific date", strict: true, input_schema: { type: "object", properties: { destination: { type: "string" }, date: { type: "string", format: "date" } }, required: ["destination", "date"], additionalProperties: false } } ] }); // Claude may call the tool first (tool_use) or respond with JSON (text) console.log("Stop reason:", response.stop_reason); for (const block of response.content) { if (block.type === "tool_use") { console.log(`Tool call: ${block.name}(${JSON.stringify(block.input)})`); } else if (block.type === "text") { console.log("Response:", block.text); } } ``` ```csharp C# var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [new() { Role = Role.User, Content = "Help me plan a trip to Paris departing May 15, 2026" }], // JSON outputs: structured response format OutputConfig = new OutputConfig { Format = new JsonOutputFormat { Schema = new Dictionary { ["type"] = JsonSerializer.SerializeToElement("object"), ["properties"] = JsonSerializer.SerializeToElement(new { summary = new { type = "string" }, next_steps = new { type = "array", items = new { type = "string" } }, }), ["required"] = JsonSerializer.SerializeToElement(new[] { "summary", "next_steps" }), ["additionalProperties"] = JsonSerializer.SerializeToElement(false), }, }, }, // Strict tool use: guaranteed tool parameters Tools = [ new Tool { Name = "search_flights", Strict = true, InputSchema = new InputSchema(new Dictionary { ["properties"] = JsonSerializer.SerializeToElement(new Dictionary { ["destination"] = new { type = "string" }, ["date"] = new { type = "string", format = "date" }, }), ["required"] = JsonSerializer.SerializeToElement(new[] { "destination", "date" }), ["additionalProperties"] = JsonSerializer.SerializeToElement(false), }), } ], }; var message = await client.Messages.Create(parameters); Console.WriteLine(message); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Help me plan a trip to Paris departing May 15, 2026")), }, // JSON outputs: structured response format OutputConfig: anthropic.OutputConfigParam{ Format: anthropic.JSONOutputFormatParam{ Schema: map[string]any{ "type": "object", "additionalProperties": false, "properties": map[string]any{ "summary": map[string]any{"type": "string"}, "next_steps": map[string]any{"type": "array", "items": map[string]any{"type": "string"}}, }, "required": []string{"summary", "next_steps"}, }, }, }, // Strict tool use: guaranteed tool parameters Tools: []anthropic.ToolUnionParam{ {OfTool: &anthropic.ToolParam{ Name: "search_flights", Strict: anthropic.Bool(true), InputSchema: anthropic.ToolInputSchemaParam{ Properties: map[string]any{ "destination": map[string]any{"type": "string"}, "date": map[string]any{"type": "string", "format": "date"}, }, Required: []string{"destination", "date"}, ExtraFields: map[string]any{ "additionalProperties": false, }, }}}, }, }) if err != nil { log.Fatal(err) } fmt.Println(response.Content) ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); // JSON outputs: structured response format JsonOutputFormat.Schema outputSchema = JsonOutputFormat.Schema.builder() .putAdditionalProperty("type", JsonValue.from("object")) .putAdditionalProperty("properties", JsonValue.from(Map.of( "summary", Map.of("type", "string"), "next_steps", Map.of("type", "array", "items", Map.of("type", "string")) ))) .putAdditionalProperty("required", JsonValue.from(List.of("summary", "next_steps"))) .putAdditionalProperty("additionalProperties", JsonValue.from(false)) .build(); // Strict tool use: guaranteed tool parameters InputSchema toolSchema = InputSchema.builder() .properties(JsonValue.from(Map.of( "destination", Map.of("type", "string"), "date", Map.of("type", "string", "format", "date") ))) .putAdditionalProperty("required", JsonValue.from(List.of("destination", "date"))) .putAdditionalProperty("additionalProperties", JsonValue.from(false)) .build(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addUserMessage("Help me plan a trip to Paris departing May 15, 2026") .outputConfig(OutputConfig.builder() .format(JsonOutputFormat.builder().schema(outputSchema).build()) .build()) .addTool(Tool.builder() .name("search_flights") .description("Search for available flights to a destination on a specific date") .strict(true) .inputSchema(toolSchema) .build()) .build(); Message response = client.messages().create(params); IO.println(response); ``` ```php PHP use Anthropic\Lib\Concerns\StructuredOutputModelTrait; use Anthropic\Lib\Contracts\StructuredOutputModel; use Anthropic\Messages\ToolUseBlock; $client = new Client(); class TripPlan implements StructuredOutputModel { use StructuredOutputModelTrait; public string $summary; public array $next_steps; } $message = $client->messages->create( maxTokens: 1024, messages: [ ['role' => 'user', 'content' => 'Help me plan a trip to Paris departing May 15, 2026'] ], model: 'claude-opus-5', // JSON outputs: structured response format outputConfig: ['format' => TripPlan::class], // Strict tool use: guaranteed tool parameters tools: [ [ 'name' => 'search_flights', 'strict' => true, 'input_schema' => [ 'type' => 'object', 'properties' => [ 'destination' => ['type' => 'string'], 'date' => ['type' => 'string', 'format' => 'date'] ], 'required' => ['destination', 'date'], 'additionalProperties' => false ] ] ], ); // Claude may call the tool first (tool_use) or respond with JSON (text) $plan = $message->parsedOutput(); if ($plan instanceof TripPlan) { echo $plan->summary, "\n"; } elseif ($toolUse = array_find($message->content, fn($block) => $block instanceof ToolUseBlock)) { echo "Tool call: {$toolUse->name}(", json_encode($toolUse->input), ")\n"; } ``` ```ruby Ruby client = Anthropic::Client.new message = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [ {role: "user", content: "Help me plan a trip to Paris departing May 15, 2026"} ], # JSON outputs: structured response format output_config: { format: { type: :json_schema, schema: { type: "object", properties: { summary: {type: "string"}, next_steps: {type: "array", items: {type: "string"}} }, required: ["summary", "next_steps"], additionalProperties: false } } }, # Strict tool use: guaranteed tool parameters tools: [ { name: "search_flights", strict: true, input_schema: { type: "object", properties: { destination: {type: "string"}, date: {type: "string", format: "date"} }, required: ["destination", "date"], additionalProperties: false } } ] ) puts message ``` ## Important considerations ### Grammar compilation and caching Structured outputs use constrained sampling with compiled grammar artifacts. This introduces some performance characteristics to be aware of: * **First request latency:** The first time you use a specific schema, there is additional latency while the grammar compiles * **Automatic caching:** Compiled grammars are cached for 24 hours from last use, making subsequent requests much faster * **Cache invalidation:** The cache is invalidated if you change: * The JSON schema structure * The set of tools in your request (when using both structured outputs and tool use) * Changing only `name` or `description` fields does not invalidate the cache ### Prompt modification and token costs When using structured outputs, Claude automatically receives an additional system prompt explaining the expected output format. This means: * Your input token count is slightly higher * The injected prompt costs you tokens like any other system prompt * Changing the `output_config.format` parameter will invalidate any [prompt cache](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) for that conversation thread ### JSON Schema limitations Structured outputs support standard JSON Schema with some limitations. Both JSON outputs and strict tool use share these limitations. * All basic types: object, array, string, integer, number, boolean, null * `enum` (strings, numbers, bools, or nulls only - no complex types; see [Invalid outputs](https://platform.claude.com/docs/en/build-with-claude/structured-outputs#invalid-outputs) for a capitalization caveat) * `const` * `anyOf` and `allOf` (with limitations - `allOf` with `$ref` not supported) * `$ref`, `$def`, and `definitions` (external `$ref` not supported) * `default` property for all supported types * `required` and `additionalProperties` (must be set to `false` for objects) * String formats: `date-time`, `time`, `date`, `duration`, `email`, `hostname`, `uri`, `ipv4`, `ipv6`, `uuid` * Array `minItems` (only values 0 and 1 supported) * Recursive schemas * Complex types within enums * External `$ref` (for example, `'$ref': 'http://...'`) * Numerical constraints (such as `minimum`, `maximum`, `multipleOf`) * String constraints (`minLength`, `maxLength`) * Array constraints beyond `minItems` of 0 or 1 * `additionalProperties` set to anything other than `false` If you use an unsupported feature, you'll receive a 400 error with details. **Supported regex features:** * Full matching (`^...$`) and partial matching * Quantifiers: `*`, `+`, `?`, simple `{n,m}` cases * Character classes: `[]`, `.`, `\d`, `\w`, `\s` * Groups: `(...)` **NOT supported:** * Backreferences to groups (for example, `\1`, `\2`) * Lookahead/lookbehind assertions (for example, `(?=...)`, `(?!...)`) * Word boundaries: `\b`, `\B` * Complex `{n,m}` quantifiers with large ranges Simple regex patterns work well. Complex patterns may result in 400 errors. The Python, TypeScript, Ruby, and PHP SDKs can automatically transform schemas with unsupported features by removing them and adding constraints to field descriptions. The C# and Go SDKs do the same when the schema is derived from a native type. See [SDK-specific methods](https://platform.claude.com/docs/en/build-with-claude/structured-outputs#sdk-specific-methods) for details. ### Property ordering When using structured outputs, properties in objects maintain their defined ordering from your schema, with one important caveat: **required properties appear first, followed by optional properties**. For example, given this schema: ```json { "type": "object", "properties": { "notes": { "type": "string" }, "name": { "type": "string" }, "email": { "type": "string" }, "age": { "type": "integer" } }, "required": ["name", "email"], "additionalProperties": false } ``` The output will order properties as: 1. `name` (required, in schema order) 2. `email` (required, in schema order) 3. `notes` (optional, in schema order) 4. `age` (optional, in schema order) This means the output might look like: ```json { "name": "John Smith", "email": "john@example.com", "notes": "Interested in enterprise plan", "age": 35 } ``` If property order in the output is important to your application, mark all properties as required, or account for this reordering in your parsing logic. ### Invalid outputs While structured outputs guarantee schema compliance in most cases, there are scenarios where the output may not match your schema: **Refusals** (`stop_reason: "refusal"`) Claude maintains its safety and helpfulness properties even when using structured outputs. If Claude refuses a request for safety reasons: * The response has `stop_reason: "refusal"` * You'll receive a 200 status code * You'll be billed for the tokens generated * The output may not match your schema because the refusal message takes precedence over schema constraints **Token limit reached** (`stop_reason: "max_tokens"`) If the response is cut off due to reaching the `max_tokens` limit: * The response has `stop_reason: "max_tokens"` * The output may be incomplete and not match your schema * Retry with a higher `max_tokens` value to get the complete structured output **Enum value casing** Structured outputs don't guarantee the capitalization of string `enum` and `const` values: Claude may return a value that differs from your schema only in capitalization, typically in the first letter of a word following a space. For example, given this schema: ```json { "type": "string", "enum": ["Conversation Topic 1", "Conversation Topic 2", "Conversation topic 3"] } ``` The output may contain `"Conversation Topic 3"` (capital "T") even though that exact value isn't in the enum. The response completes normally, with no error and no special `stop_reason`. This applies to both JSON outputs and strict tool use. Compare enum values case-insensitively, and avoid enum values that differ only in capitalization. ### Schema complexity limits Structured outputs work by compiling your JSON schemas into a grammar that constrains Claude's output. More complex schemas produce larger grammars that take longer to compile. To protect against excessive compilation times, the API enforces several complexity limits. #### Explicit limits The following limits apply to all requests with `output_config.format` or `strict: true`: | Limit | Value | Description | | --------------------------- | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Strict tools per request | 20 | Maximum number of tools with `strict: true`. Non-strict tools don't count toward this limit. | | Optional parameters | 24 | Total optional parameters across all strict tool schemas and JSON output schemas. Each parameter not listed in `required` counts toward this limit. | | Parameters with union types | 16 | Total parameters that use `anyOf` or type arrays (for example, `"type": ["string", "null"]`) across all strict schemas. These are especially expensive because they create exponential compilation cost. | These limits apply to the combined total across all strict schemas in a single request. For example, if you have 4 strict tools with 6 optional parameters each, you'll reach the 24-parameter limit even though no single tool seems complex. #### Additional internal limits Beyond the explicit limits in the preceding table, there are additional internal limits on the compiled grammar size. These limits exist because schema complexity doesn't reduce to a single dimension: features like optional parameters, union types, nested objects, and number of tools interact with each other in ways that can make the compiled grammar disproportionately large. When these limits are exceeded, you'll receive a 400 error with the message "Schema is too complex for compilation." These errors mean the combined complexity of your schemas exceeds what can be efficiently compiled, even if each individual limit in the preceding table is satisfied. As a final stop-gap, the API also enforces a **compilation timeout of 180 seconds**. Schemas that pass all explicit checks but produce very large compiled grammars may hit this timeout. #### Tips for reducing schema complexity If you're hitting complexity limits, try these strategies in order: 1. **Mark only critical tools as strict.** If you have many tools, reserve it for tools where schema violations cause real problems, and rely on Claude's natural adherence for simpler tools. 2. **Reduce optional parameters.** Make parameters `required` where possible. Each optional parameter roughly doubles a portion of the grammar's state space. If a parameter always has a reasonable default, consider making it required and having Claude provide that default explicitly. 3. **Simplify nested structures.** Deeply nested objects with optional fields compound the complexity. Flatten structures where possible. 4. **Split into multiple requests.** If you have many strict tools, consider splitting them across separate requests or sub-agents. For persistent issues with valid schemas, [contact support](https://support.claude.com/en/articles/9015913-how-to-get-support) with your schema definition. ## Data retention Prompts and responses are processed with ZDR when using structured outputs. However, the JSON schema itself is temporarily cached for up to 24 hours since last use for optimization purposes. No prompt or response data is retained beyond the API response. Structured outputs are HIPAA eligible, but **PHI must not be included in JSON schema definitions**. The API compiles JSON schemas into grammars that are cached separately from message content, and these cached schemas do not receive the same PHI protections as prompts and responses. Do not include PHI in schema property names, `enum` values, `const` values, or `pattern` regular expressions. PHI should only appear in message content (prompts and responses), where it is protected under HIPAA safeguards. For ZDR and HIPAA eligibility across all features, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention). ## Feature compatibility **Works with:** * **[Batch processing](https://platform.claude.com/docs/en/build-with-claude/batch-processing):** Process structured outputs at scale with 50% discount * **[Token counting](https://platform.claude.com/docs/en/build-with-claude/token-counting):** Count tokens without compilation * **[Streaming](https://platform.claude.com/docs/en/build-with-claude/streaming):** Stream structured outputs like normal responses * **Combined usage:** Use JSON outputs (`output_config.format`) and strict tool use (`strict: true`) together in the same request **Incompatible with:** * **[Citations](https://platform.claude.com/docs/en/build-with-claude/citations):** Citations require interleaving citation blocks with text, which conflicts with strict JSON schema constraints. Returns 400 error if citations enabled with `output_config.format`. * **Message Prefilling:** Incompatible with JSON outputs **Grammar scope:** Grammars apply only to Claude's direct output, not to tool use calls, tool results, or thinking tags (when using [thinking](https://platform.claude.com/docs/en/build-with-claude/thinking)). Grammar state resets between sections, allowing Claude to think freely while still producing structured output in the final response. ## Next steps Have Claude cite its sources when answering questions about provided documents. Enforce JSON Schema compliance on Claude's tool inputs with grammar-constrained sampling. Connect Claude to external tools and APIs. Learn where tools execute and how the agentic loop works. Learn about Anthropic's pricing structure for models and features. --- title: Task budgets url: https://platform.claude.com/docs/en/build-with-claude/task-budgets description: Give Claude an advisory token budget for the full agentic loop to help the model self-regulate on long agentic tasks. --- ## Compatibility - Status: Beta - [Beta header](https://platform.claude.com/docs/en/api/beta-headers): `task-budgets-2026-03-13` - Supported models: `claude-fable-5`, `claude-mythos-5`, `claude-opus-5`, `claude-opus-4-8`, `claude-opus-4-7` Task budgets let you tell Claude how many tokens it has for a full agentic loop, including thinking, tool calls, tool results, and output. The model sees a running countdown and uses it to prioritize work and finish gracefully as the budget is consumed. ## When to use task budgets Task budgets work best for agentic workflows where Claude makes multiple tool calls and decisions before finalizing its output to await the next human response. Use them when: * You want Claude to self-regulate token spend on long-horizon tasks. * You have a predictable per-task cost or latency ceiling to enforce. * You want the model to finish gracefully (summarize findings, report progress) as it approaches the budget rather than cutting off mid-action. Task budgets complement the [effort parameter](https://platform.claude.com/docs/en/build-with-claude/effort): effort controls how thoroughly Claude reasons about each step, while task budgets cap the total work Claude can do across an agentic loop. ## Setting a task budget Add `task_budget` to `output_config` and include the beta header: ```bash cURL curl https://api.anthropic.com/v1/messages \ -N \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: task-budgets-2026-03-13" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 128000, "stream": true, "messages": [{ "role": "user", "content": "Review the codebase and propose a refactor plan." }], "output_config": { "effort": "high", "task_budget": {"type": "tokens", "total": 64000} } }' ``` ```bash CLI ant beta:messages create --beta task-budgets-2026-03-13 \ --stream --format jsonl <<'YAML' | jq 'select(.type == "message_delta").usage' model: claude-opus-5 max_tokens: 128000 messages: - role: user content: Review the codebase and propose a refactor plan. output_config: effort: high task_budget: type: tokens total: 64000 YAML ``` ```python Python client = anthropic.Anthropic() with client.beta.messages.stream( model="claude-opus-5", max_tokens=128000, output_config={ "effort": "high", "task_budget": {"type": "tokens", "total": 64000}, }, messages=[ {"role": "user", "content": "Review the codebase and propose a refactor plan."} ], betas=["task-budgets-2026-03-13"], ) as stream: response = stream.get_final_message() print(response.usage) ``` ```typescript TypeScript const client = new Anthropic(); const stream = client.beta.messages.stream({ model: "claude-opus-5", max_tokens: 128000, output_config: { effort: "high", task_budget: { type: "tokens", total: 64000 } }, messages: [{ role: "user", content: "Review the codebase and propose a refactor plan." }], betas: ["task-budgets-2026-03-13"] }); const response = await stream.finalMessage(); console.log(response.usage); ``` ```csharp C# var client = new AnthropicClient(); var responseUpdates = client.Beta.Messages.CreateStreaming(new MessageCreateParams { Model = Messages::Model.ClaudeOpus5, MaxTokens = 128000, Messages = [new() { Role = Role.User, Content = "Review the codebase and propose a refactor plan." }], OutputConfig = new BetaOutputConfig { Effort = Effort.High, TaskBudget = new BetaTokenTaskBudget { Total = 64000 }, }, Betas = ["task-budgets-2026-03-13"], }); var response = await responseUpdates.Aggregate(); Console.WriteLine(response.Usage); ``` ```go Go client := anthropic.NewClient() stream := client.Beta.Messages.NewStreaming(context.TODO(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 128000, Betas: []anthropic.AnthropicBeta{"task-budgets-2026-03-13"}, Messages: []anthropic.BetaMessageParam{{ Role: anthropic.BetaMessageParamRoleUser, Content: []anthropic.BetaContentBlockParamUnion{{ OfText: &anthropic.BetaTextBlockParam{Text: "Review the codebase and propose a refactor plan."}, }}, }}, OutputConfig: anthropic.BetaOutputConfigParam{ Effort: anthropic.BetaOutputConfigEffortHigh, TaskBudget: anthropic.BetaTokenTaskBudgetParam{ Total: 64000, }, }, }) message := anthropic.BetaMessage{} for stream.Next() { event := stream.Current() if err := message.Accumulate(event); err != nil { panic(err) } } if stream.Err() != nil { panic(stream.Err()) } fmt.Printf("Usage: input_tokens=%d, output_tokens=%d\n", message.Usage.InputTokens, message.Usage.OutputTokens) ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(128000L) .addUserMessage("Review the codebase and propose a refactor plan.") .outputConfig(BetaOutputConfig.builder() .effort(BetaOutputConfig.Effort.HIGH) .taskBudget(BetaTokenTaskBudget.builder().total(64000L).build()) .build()) .addBeta("task-budgets-2026-03-13") .build(); BetaMessageAccumulator accumulator = BetaMessageAccumulator.create(); try (StreamResponse stream = client.beta().messages().createStreaming(params)) { stream.stream().forEach(accumulator::accumulate); } BetaMessage response = accumulator.message(); IO.println(response.usage()); ``` ```php PHP use Anthropic\Beta\Messages\BetaRawMessageDeltaEvent; $client = new Client(); $stream = $client->beta->messages->createStream( model: 'claude-opus-5', maxTokens: 128000, messages: [ ['role' => 'user', 'content' => 'Review the codebase and propose a refactor plan.'], ], outputConfig: [ 'effort' => 'high', 'taskBudget' => ['type' => 'tokens', 'total' => 64000], ], betas: ['task-budgets-2026-03-13'], ); // The final message_delta event carries the cumulative token usage for the request. $usage = null; foreach ($stream as $event) { if ($event instanceof BetaRawMessageDeltaEvent) { $usage = $event->usage; } } echo $usage; ``` ```ruby Ruby client = Anthropic::Client.new stream = client.beta.messages.stream( model: "claude-opus-5", max_tokens: 128_000, messages: [ { role: "user", content: "Review the codebase and propose a refactor plan." } ], output_config: { effort: :high, task_budget: { type: :tokens, total: 64_000 } }, betas: ["task-budgets-2026-03-13"] ) response = stream.accumulated_message puts response.usage ``` The `task_budget` object has three fields: * `type`: always `"tokens"`. * `total`: the number of tokens Claude can spend across the agentic loop, including thinking, tool calls, tool results, and output. * `remaining` (optional): the budget remainder carried over from a prior request. Defaults to `total` when omitted. ## How the budget countdown works Claude sees a budget-countdown marker injected server-side throughout the conversation. The marker shows how many tokens remain in the current agentic loop and updates as the model generates thinking, tool calls, and output, and as it processes tool results. Claude uses this signal to pace itself and finish gracefully as the budget is consumed. **The countdown is visible only to the model.** API responses do not include a remaining-budget field: there is no `task_budget` information in the response `usage` object, and SDKs have no accessor for it. To track spend client-side, sum token usage across the requests in your loop as shown in [Measure your current usage](https://platform.claude.com/docs/en/build-with-claude/task-budgets#measure-your-current-usage), or pass your own figure forward with `remaining` when [carrying a budget across compaction](https://platform.claude.com/docs/en/build-with-claude/task-budgets#carrying-a-budget-across-compaction-with-remaining). **The countdown reflects tokens Claude has processed in the current agentic loop, not tokens you resend between turns.** If your client sends the full conversation history on every follow-up request, your client-side token count may differ from the budget Claude is tracking. If you also decrement `remaining` while resending full history, the model sees an under-reported budget and the countdown drops faster than it should, causing Claude to wrap up earlier than the budget actually allows. Set a generous budget and let the model self-regulate against the countdown rather than trying to mirror it client-side. ### Worked example: budget counting across turns The task budget counts what Claude **sees** (thinking, tool calls and results, and text), not what's in your request payload. In an agentic loop your client resends the full conversation on every request, so the payload grows turn over turn, but the budget only decrements by the tokens Claude sees this turn. Consider a loop with `task_budget: {type: "tokens", total: 100000}` and a single `bash` tool. **Turn 1.** You send the initial request: ```json { "messages": [ { "role": "user", "content": "Audit this repo for security issues and report findings." } ] } ``` Claude thinks, then emits a tool call and stops with `stop_reason: "tool_use"`: ```json { "role": "assistant", "content": [ { "type": "thinking", "thinking": "I'll start by listing dependencies to look for known-vulnerable packages..." }, { "type": "tool_use", "id": "toolu_01", "name": "bash", "input": { "command": "cat package.json && npm audit --json" } } ] } ``` Suppose this assistant turn (thinking plus the tool call) totals 5,000 generated tokens. The countdown Claude saw during generation ended near `remaining` ≈ 95,000. **Turn 2.** Your client runs the tool, then resends the full history with the tool result appended: ```json { "messages": [ { "role": "user", "content": "Audit this repo for security issues and report findings." }, { "role": "assistant", "content": [ { "type": "thinking", "thinking": "I'll start by listing dependencies..." }, { "type": "tool_use", "id": "toolu_01", "name": "bash", "input": { "command": "cat package.json && npm audit --json" } } ] }, { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_01", "content": "<2,800 tokens of npm audit output>" } ] } ] } ``` The resent turn-1 user and assistant messages are not counted again, but the 2,800-token tool result is new content Claude sees this turn and counts against the budget. Claude spends another 4,000 tokens on thinking and a second tool call (`grep -rn "eval(" src/`). The countdown ends near `remaining` ≈ 88,200. **Turn 3.** Full history resent again with the second tool result (1,200 tokens of grep output) appended. Claude writes a 6,000-token final findings report and stops with `stop_reason: "end_turn"`. `remaining` ≈ 81,000. Putting the three turns side by side makes the distinction between payload size and budget spend explicit: | Turn | Request payload (approx. input tokens you sent) | Tokens counted against budget this turn | Budget `remaining` after | | --------- | ----------------------------------------------- | --------------------------------------------------------- | ------------------------ | | 1 | \~20 | 5,000 (thinking + `tool_use`) | \~95,000 | | 2 | \~7,800 (turn 1 history + tool result) | 6,800 (2,800 tool result + 4,000 thinking and `tool_use`) | \~88,200 | | 3 | \~13,000 (full history + second tool result) | 7,200 (1,200 tool result + 6,000 `text`) | \~81,000 | | **Total** | **\~20,820 sent across requests** | **19,000 counted against budget** | N/A | Your client sent the turn-1 user message three times and the turn-1 assistant message twice, but each was counted once. The budget spent 19,000 of 100,000 tokens, even though the cumulative payload your client transmitted was larger and the prompt-cached input on turns 2 and 3 was larger still. ### Carrying a budget across compaction with `remaining` If your agentic loop compacts or rewrites context between requests (for example, by summarizing earlier turns), the server has no memory of how much budget was spent before compaction. Pass `remaining` on the next request so the countdown continues from where you left off rather than resetting to `total`: ```python Python # Tokens spent before compaction, tracked client-side tokens_spent_so_far = 45000 output_config = { "effort": "high", "task_budget": { "type": "tokens", "total": 128000, "remaining": 128000 - tokens_spent_so_far, }, } ``` ```typescript TypeScript // Tokens spent before compaction, tracked client-side const tokensSpentSoFar = 45000; const outputConfig = { effort: "high", task_budget: { type: "tokens", total: 128000, remaining: 128000 - tokensSpentSoFar } }; ``` ```csharp C# // Tokens spent before compaction, tracked client-side var tokensSpentSoFar = 45000; var outputConfig = new BetaOutputConfig { Effort = Effort.High, TaskBudget = new BetaTokenTaskBudget { Total = 128000, Remaining = 128000 - tokensSpentSoFar, }, }; ``` ```go Go // Tokens spent before compaction, tracked client-side tokensSpentSoFar := int64(45000) outputConfig := anthropic.BetaOutputConfigParam{ Effort: anthropic.BetaOutputConfigEffortHigh, TaskBudget: anthropic.BetaTokenTaskBudgetParam{ Total: 128000, Remaining: anthropic.Int(128000 - tokensSpentSoFar), }, } ``` ```java Java // Tokens spent before compaction, tracked client-side long tokensSpentSoFar = 45000; BetaOutputConfig outputConfig = BetaOutputConfig.builder() .effort(BetaOutputConfig.Effort.HIGH) .taskBudget(BetaTokenTaskBudget.builder() .total(128000L) .remaining(128000L - tokensSpentSoFar) .build()) .build(); ``` ```php PHP // Tokens spent before compaction, tracked client-side $tokensSpentSoFar = 45000; $outputConfig = [ 'effort' => 'high', 'taskBudget' => [ 'type' => 'tokens', 'total' => 128000, 'remaining' => 128000 - $tokensSpentSoFar, ], ]; ``` ```ruby Ruby # Tokens spent before compaction, tracked client-side tokens_spent_so_far = 45_000 output_config = { effort: :high, task_budget: { type: :tokens, total: 128_000, remaining: 128_000 - tokens_spent_so_far } } ``` For loops that resend the full uncompacted history on every turn, omit `remaining` and let the server track the countdown. ## Changing the budget mid-conversation `task_budget` is a request-level setting. To change the budget partway through a task, for example to extend it when the user broadens the request, set a new `task_budget` in `output_config` on the next request. Keep the caching consequence in mind: the budget value participates in the rendered prompt, so a changed value does not match cache entries created under the old one (see [Feature support](https://platform.claude.com/docs/en/build-with-claude/task-budgets#feature-support) below). ## Task budgets are advisory, not enforced Task budgets are a **soft hint, not a hard cap**. Claude may occasionally exceed the budget if it is in the middle of an action that would be more disruptive to interrupt than to finish. The enforced limit on total output tokens is still `max_tokens`, which truncates the response with `stop_reason: "max_tokens"` when reached. For a hard cap on cost or latency, combine task budgets with a reasonable `max_tokens` value: * Use `task_budget` to give Claude a target to pace against. * Use `max_tokens` as the absolute ceiling that prevents runaway generation. Because `task_budget` spans the full agentic loop (potentially many requests) while `max_tokens` caps each individual request, the two values are independent; one is not required to be at or below the other. **A budget that is too small for the task can cause refusal-like behavior.** When Claude sees a budget that is clearly insufficient for the work being asked (for example, a 20,000-token budget for a multihour agentic coding task), it may decline to attempt the task at all, scope it down aggressively, or stop early with a partial result rather than start work it cannot finish. If you observe unexpected refusals or premature stops after setting a budget, raise the budget before debugging other parameters. Size budgets against your actual task-length distribution rather than a fixed default; see [Choosing a budget](https://platform.claude.com/docs/en/build-with-claude/task-budgets#choosing-a-budget). ## Choosing a budget The right budget depends on how much work your agentic loop currently does. Rather than guessing, measure your existing token usage first and then tune from there. ### Measure your current usage Run a representative sample of tasks **without** `task_budget` set and record the total tokens Claude spends per task. For an agentic loop, sum `usage.output_tokens` across every request in the loop, plus the tokens of the tool results you append between requests: ```bash CLI ant messages create --transform 'usage.output_tokens' <<'YAML' model: claude-opus-5 max_tokens: 4096 messages: - role: user content: Review the codebase and propose a refactor plan. YAML ``` ```python Python client = anthropic.Anthropic() response = client.messages.create( model="claude-opus-5", max_tokens=4096, messages=[ {"role": "user", "content": "Review the codebase and propose a refactor plan."} ], ) # Sum output_tokens (text + thinking + tool calls) across every request in your loop. print(response.usage.output_tokens) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 4096, messages: [{ role: "user", content: "Review the codebase and propose a refactor plan." }] }); // Sum output_tokens (text + thinking + tool calls) across every request in your loop. console.log(response.usage.output_tokens); ``` ```csharp C# var client = new AnthropicClient(); var response = await client.Messages.Create(new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 4096, Messages = [new() { Role = Role.User, Content = "Review the codebase and propose a refactor plan." }], }); // Sum OutputTokens (text + thinking + tool calls) across every request in your loop. Console.WriteLine(response.Usage.OutputTokens); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 4096, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Review the codebase and propose a refactor plan.")), }, }) if err != nil { log.Fatal(err) } // Sum OutputTokens (text + thinking + tool calls) across every request in your loop. fmt.Println(response.Usage.OutputTokens) ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(4096L) .addUserMessage("Review the codebase and propose a refactor plan.") .build(); Message response = client.messages().create(params); // Sum outputTokens (text + thinking + tool calls) across every request in your loop. IO.println(response.usage().outputTokens()); ``` ```php PHP $client = new Client(); $response = $client->messages->create( model: 'claude-opus-5', maxTokens: 4096, messages: [ ['role' => 'user', 'content' => 'Review the codebase and propose a refactor plan.'], ], ); // Sum outputTokens (text + thinking + tool calls) across every request in your loop. echo $response->usage->outputTokens . "\n"; ``` ```ruby Ruby client = Anthropic::Client.new response = client.messages.create( model: "claude-opus-5", max_tokens: 4096, messages: [ { role: "user", content: "Review the codebase and propose a refactor plan." } ] ) # Sum output_tokens (text + thinking + tool calls) across every request in your loop. puts response.usage.output_tokens ``` Run this across a representative set of tasks and record the distribution. Start with the p99 of your per-task token spend to understand how providing the model with a task budget might modify the model's behavior, then test up or down as needed. The minimum accepted `task_budget.total` is model-specific; on every model that currently supports task budgets (see [Feature support](https://platform.claude.com/docs/en/build-with-claude/task-budgets#feature-support)) it is **20,000 tokens**, and values below the minimum return a 400 error. ## Interaction with other parameters * **`max_tokens`:** Orthogonal to task budgets. `max_tokens` is a hard per-request cap on generated tokens, while `task_budget` is an advisory cap across the full agentic loop (potentially spanning many requests). At `xhigh` or `max` effort, set `max_tokens` to at least 64k to give Claude room to think and act on each request. * **[Effort](https://platform.claude.com/docs/en/build-with-claude/effort):** Effort controls how deeply Claude reasons per step. Task budgets control how much total work Claude does across an agentic loop. The two are complementary: effort tunes depth, task budgets tune breadth. * **[Adaptive thinking](https://platform.claude.com/docs/en/build-with-claude/thinking):** Task budgets include thinking tokens in the count, so adaptive thinking naturally scales down as the budget depletes. * **[Prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching):** The budget-countdown marker is injected server-side per turn, so it does not match across requests. If your client decrements `task_budget.remaining` on each follow-up request, the changed value invalidates any cache prefix that contains it. To preserve caching, set the budget once on the initial request and let the model self-regulate against the server-side countdown rather than mutating the budget client-side. ## Feature support | Model | Support | | ----------------- | ------------------------------------------- | | Claude Opus 5 | Beta (set `task-budgets-2026-03-13` header) | | Claude Fable 5 | Beta (set `task-budgets-2026-03-13` header) | | Claude Mythos 5 | Beta (set `task-budgets-2026-03-13` header) | | Claude Sonnet 5 | Not supported | | Claude Opus 4.8 | Beta (set `task-budgets-2026-03-13` header) | | Claude Opus 4.7 | Beta (set `task-budgets-2026-03-13` header) | | Claude Opus 4.6 | Not supported | | Claude Sonnet 4.6 | Not supported | | Claude Haiku 4.5 | Not supported | Task budgets are not supported on [Claude Code](https://code.claude.com/docs/en/overview) or Cowork surfaces. Use task budgets directly through the Messages API on a [supported model](https://platform.claude.com/docs/en/build-with-claude/task-budgets#feature-support). ## Next steps Control how thoroughly Claude reasons about each step of an agentic loop. Let Claude decide when and how much to use extended thinking. Manage context in long-running conversations with server-side compaction. Reduce cost and latency on repeated prompts by caching prompt prefixes. ### Model capabilities > Thinking --- title: Extended thinking url: https://platform.claude.com/docs/en/build-with-claude/extended-thinking description: Configure manual extended thinking with a fixed budget_tokens budget on Claude models that support it, and migrate to adaptive thinking. --- For how zero data retention (ZDR) applies to this feature, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention). Extended thinking (`thinking.type: "enabled"` with `budget_tokens`) is deprecated on the Claude 4.6 models (requests using it still succeed). Claude 4.7 and later models do not support it and reject requests that use it, returning a 400 error. On Claude 4.5 and earlier models that support thinking, extended thinking is the only available thinking mode. Claude Mythos Preview supports both modes. Where both modes are available, use [adaptive thinking](https://platform.claude.com/docs/en/build-with-claude/thinking) instead. See [Migrating to adaptive thinking](https://platform.claude.com/docs/en/build-with-claude/extended-thinking#migrating-to-adaptive-thinking) to move to adaptive thinking. If your model supports only extended thinking, this page describes the supported configuration; no change is needed until you move to a newer model. If a request fails with a 400 error whose message starts with `"thinking.type.enabled" is not supported`, your model uses adaptive thinking instead. See [Troubleshooting thinking](https://platform.claude.com/docs/en/build-with-claude/thinking-troubleshooting#error-thinking-type-enabled), or jump to [Migrating to adaptive thinking](https://platform.claude.com/docs/en/build-with-claude/extended-thinking#migrating-to-adaptive-thinking). Extended thinking in manual mode gives you direct control over how much Claude thinks. You set a thinking token budget on each request with `thinking: {type: "enabled", budget_tokens: N}`, and Claude thinks against that budget before it starts its final answer. Manual mode remains useful when your workload requires predictable latency or precise control over thinking costs. This page covers how to set and tune the budget, how manual mode interacts with interleaved thinking and prompt caching, and how to migrate to adaptive thinking. For how thinking itself works, including thinking blocks and the response shape, the `display` parameter, streaming, thinking with tool use, and encryption, see the [thinking overview](https://platform.claude.com/docs/en/build-with-claude/thinking). ## Supported models Extended thinking availability per model, including the models where extended thinking is the only mode, is listed in the [per-model configuration table](https://platform.claude.com/docs/en/build-with-claude/thinking-troubleshooting#supported-models). ## How to use extended thinking Here is an example of using extended thinking in the Messages API: ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-sonnet-4-6", "max_tokens": 16000, "thinking": { "type": "enabled", "budget_tokens": 10000 }, "messages": [ { "role": "user", "content": "Are there an infinite number of prime numbers such that n mod 4 == 3?" } ] }' ``` ```bash CLI ant messages create \ --transform content --format yaml <<'YAML' model: claude-sonnet-4-6 max_tokens: 16000 thinking: type: enabled budget_tokens: 10000 messages: - role: user content: Are there an infinite number of prime numbers such that n mod 4 == 3? YAML ``` ```python Python client = anthropic.Anthropic() response = client.messages.create( model="claude-sonnet-4-6", max_tokens=16000, thinking={"type": "enabled", "budget_tokens": 10000}, messages=[ { "role": "user", "content": "Are there an infinite number of prime numbers such that n mod 4 == 3?", } ], ) # The response contains summarized thinking blocks and text blocks for block in response.content: match block.type: case "thinking": print(f"\nThinking summary: {block.thinking}") case "text": print(f"\nResponse: {block.text}") ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.messages.create({ model: "claude-sonnet-4-6", max_tokens: 16000, thinking: { type: "enabled", budget_tokens: 10000, }, messages: [ { role: "user", content: "Are there an infinite number of prime numbers such that n mod 4 == 3?", }, ], }); // The response contains summarized thinking blocks and text blocks for (const block of response.content) { if (block.type === "thinking") { console.log(`\nThinking summary: ${block.thinking}`); } else if (block.type === "text") { console.log(`\nResponse: ${block.text}`); } } ``` ```csharp C# AnthropicClient client = new(); var response = await client.Messages.Create(new() { Model = Model.ClaudeSonnet4_6, MaxTokens = 16000, Thinking = new ThinkingConfigEnabled(budgetTokens: 10000), Messages = [ new() { Role = Role.User, Content = "Are there an infinite number of prime numbers such that n mod 4 == 3?", }, ], }); // The response contains summarized thinking blocks and text blocks foreach (var block in response.Content) { if (block.TryPickThinking(out var thinking)) { Console.WriteLine($"\nThinking summary: {thinking.Thinking}"); } else if (block.TryPickText(out var text)) { Console.WriteLine($"\nResponse: {text.Text}"); } } ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.Background(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeSonnet4_6, MaxTokens: 16000, Thinking: anthropic.ThinkingConfigParamOfEnabled(10000), Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Are there an infinite number of prime numbers such that n mod 4 == 3?")), }, }) if err != nil { log.Fatal(err) } // The response contains summarized thinking blocks and text blocks for _, block := range response.Content { switch block := block.AsAny().(type) { case anthropic.ThinkingBlock: fmt.Printf("\nThinking summary: %s", block.Thinking) case anthropic.TextBlock: fmt.Printf("\nResponse: %s", block.Text) } } ``` ```java Java import com.anthropic.client.okhttp.AnthropicOkHttpClient; import com.anthropic.models.messages.MessageCreateParams; import com.anthropic.models.messages.Model; void main() { var client = AnthropicOkHttpClient.fromEnv(); var params = MessageCreateParams.builder() .model(Model.CLAUDE_SONNET_4_6) .maxTokens(16_000) .enabledThinking(10_000) .addUserMessage("Are there an infinite number of prime numbers such that n mod 4 == 3?") .build(); var response = client.messages().create(params); // The response contains summarized thinking blocks and text blocks for (var block : response.content()) { block.thinking().ifPresent(thinkingBlock -> IO.println("\nThinking summary: " + thinkingBlock.thinking()) ); block.text().ifPresent(textBlock -> IO.println("\nResponse: " + textBlock.text()) ); } } ``` ```php PHP $client = new Client(); $response = $client->messages->create( model: 'claude-sonnet-4-6', maxTokens: 16000, thinking: ['type' => 'enabled', 'budget_tokens' => 10000], messages: [ [ 'role' => 'user', 'content' => 'Are there an infinite number of prime numbers such that n mod 4 == 3?', ], ], ); // The response contains summarized thinking blocks and text blocks foreach ($response->content as $block) { echo match ($block->type) { 'thinking' => "\nThinking summary: {$block->thinking}", 'text' => "\nResponse: {$block->text}", default => '', }; } ``` ```ruby Ruby client = Anthropic::Client.new response = client.messages.create( model: "claude-sonnet-4-6", max_tokens: 16_000, thinking: { type: :enabled, budget_tokens: 10_000 }, messages: [ { role: :user, content: "Are there an infinite number of prime numbers such that n mod 4 == 3?" } ] ) # The response contains summarized thinking blocks and text blocks response.content.each do |block| case block in {type: :thinking, thinking:} puts "\nThinking summary: #{thinking}" in {type: :text, text:} puts "\nResponse: #{text}" else end end ``` To turn on manual extended thinking, add a `thinking` object with `type` set to `enabled` and a `budget_tokens` value. The `budget_tokens` parameter sets a target for how many tokens Claude can use for its internal reasoning process. Larger budgets can improve response quality by enabling more thorough analysis for complex problems. ## Budget rules and tuning `budget_tokens` must satisfy these constraints: * **Minimum of 1,024 tokens.** The API rejects smaller values. * **Less than `max_tokens`.** Thinking tokens count toward the `max_tokens` limit for the turn, so the budget must leave room for the final response. The one exception is [interleaved thinking](https://platform.claude.com/docs/en/build-with-claude/extended-thinking#interleaved-thinking), where `budget_tokens` can exceed `max_tokens` because the budget spans all thinking blocks within one assistant turn. * **No cache pre-warming.** Because `budget_tokens` must be less than `max_tokens`, extended thinking cannot be combined with `max_tokens: 0` ([cache pre-warming](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#pre-warming-the-cache)). The budget is a target rather than a strict cap. Actual token usage varies with the task, and Claude may stop reasoning well before the budget is exhausted; `max_tokens` remains the hard ceiling on total output. On Claude Opus 4.5, the only extended-thinking-only model that supports [effort](https://platform.claude.com/docs/en/build-with-claude/effort), effort shapes the overall response while `budget_tokens` sets thinking depth; set both. To tune the budget: * Match the starting point to the task. For simple tasks, start near the 1,024-token minimum and increase incrementally to find the optimal range for your use case. For complex tasks, start with a larger budget of 16,000 tokens or more and adjust to your latency and quality needs. Higher budgets enable more comprehensive reasoning, with diminishing returns that depend on the task, and at the cost of increased latency. For critical tasks, test different settings to find the right balance. * For thinking budgets above 32k, use [batch processing](https://platform.claude.com/docs/en/build-with-claude/batch-processing) to avoid networking issues. Pushing the model to think beyond 32k tokens produces long-running requests that can hit system timeouts and open-connection limits. To track what a budget actually costs you, monitor the `usage.output_tokens_details.thinking_tokens` field in the response, which reports how many of the billed output tokens were internal reasoning. When streaming, this breakdown appears only on the final `message_delta` event. When you are ready to move off manual budgets, see [Migrating to adaptive thinking](https://platform.claude.com/docs/en/build-with-claude/extended-thinking#migrating-to-adaptive-thinking). ## Interleaved thinking in manual mode Interleaved thinking lets Claude think between tool calls within a single assistant turn, reasoning about each tool result before deciding what to do next. For the concept, the turn structure, and how it behaves on adaptive-thinking models, see [interleaved thinking](https://platform.claude.com/docs/en/build-with-claude/thinking#interleaved-thinking) in the thinking overview. This section covers how to enable it when you use manual `type: "enabled"` thinking. On Claude Opus 4.5, Claude Sonnet 4.5, and earlier Claude 4 models (Claude Opus 4.1, Claude Opus 4, and Claude Sonnet 4), add the `interleaved-thinking-2025-05-14` [beta header](https://platform.claude.com/docs/en/api/beta-headers) to your API request. The 4.6 generation splits in manual mode: * **Claude Sonnet 4.6**: the beta header with manual `type: "enabled"` is still functional but deprecated. Prefer [adaptive thinking](https://platform.claude.com/docs/en/build-with-claude/thinking), which interleaves automatically with no header. * **Claude Opus 4.6**: manual mode has no interleaved thinking at all. Only its adaptive mode interleaves, so switch to `thinking: {type: "adaptive"}` if you need reasoning between tool calls on this model. Claude Haiku 4.5 does not support interleaved thinking. On the Claude API, the beta header is accepted but ignored. Two more considerations for interleaved thinking in manual mode: * `budget_tokens` can exceed `max_tokens` here; the [budget rules](https://platform.claude.com/docs/en/build-with-claude/extended-thinking#budget-rules-and-tuning) explain this exception. * Interleaved thinking is only supported for [tools used through the Messages API](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview). How platforms treat the beta header differs. The Claude API and [Claude Platform on AWS](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws) accept `interleaved-thinking-2025-05-14` on any model and ignore it where unsupported. Acceptance is not the same as effect: on models that reject `type: "enabled"` (4.7 and later) or lack manual-mode interleaving (Claude Opus 4.6), the header has no manual-mode effect; adaptive thinking interleaves automatically there. Partner-operated platforms ([Amazon Bedrock](https://platform.claude.com/docs/en/build-with-claude/claude-in-amazon-bedrock) and [Google Cloud](https://platform.claude.com/docs/en/build-with-claude/claude-on-vertex-ai)) likewise accept the header on any model without returning an error, and ignore it on models that don't support interleaved thinking. ## Turn structure in manual mode The general turn-structure rules, including the single-turn tool-use loop, mid-turn conflict handling, and toggling thinking between turns, are on [Thinking with tool use](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-with-tool-use). Manual mode adds one requirement: the final assistant turn of a thinking-enabled request must begin with a thinking block ([adaptive thinking](https://platform.claude.com/docs/en/build-with-claude/thinking) drops that requirement). Changing the thinking configuration between turns also invalidates prompt caching; see the following section. ## Prompt caching in manual mode Manual mode adds one rule on top of the mode-neutral caching behavior described in [thinking and prompt caching](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-and-prompt-caching): changing `budget_tokens` between requests invalidates cache breakpoints, just as switching thinking modes does, because the budget value is rendered into the prompt. Message-level breakpoints always miss after a budget change; whether tool and system-prompt breakpoints miss too depends on where the model renders the configuration. In practice, pick a budget and hold it stable for the life of a cached conversation. Running a multi-turn conversation with message-level caching on Claude Sonnet 4.6 and changing the budget on the third request from 4,000 to 8,000 tokens shows the invalidation directly: ```text Output wrap First request - establishing cache First response usage: { cache_creation_input_tokens: 1370, cache_read_input_tokens: 0, input_tokens: 17, output_tokens: 700 } Second request - same thinking parameters (cache hit expected) Second response usage: { cache_creation_input_tokens: 0, cache_read_input_tokens: 1370, input_tokens: 303, output_tokens: 874 } Third request - different thinking budget (cache miss expected) Third response usage: { cache_creation_input_tokens: 1370, cache_read_input_tokens: 0, input_tokens: 747, output_tokens: 619 } ``` The third request re-creates the cache (`cache_creation_input_tokens=1370`, `cache_read_input_tokens=0`) because the budget changed between requests. For a runnable version of the same experiment in adaptive mode, where the effort level plays the cache role that `budget_tokens` plays here, see [Prompt caching](https://platform.claude.com/docs/en/build-with-claude/thinking-steering-and-cost#prompt-caching) on the steering page. ## Shared mechanics Most thinking behavior is mode neutral and documented once on the [Thinking](https://platform.claude.com/docs/en/build-with-claude/thinking) page. Everything there applies in manual mode too: * [Controlling thinking display](https://platform.claude.com/docs/en/build-with-claude/thinking#controlling-thinking-display) * [Streaming thinking](https://platform.claude.com/docs/en/build-with-claude/thinking#streaming-thinking) * [Thinking with tool use](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-with-tool-use), including [preserving thinking blocks](https://platform.claude.com/docs/en/build-with-claude/thinking#preserving-thinking-blocks) * [Thinking and prompt caching](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-and-prompt-caching) * [Thinking and the context window](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-and-the-context-window) * [Thinking encryption](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-encryption) * [Pricing](https://platform.claude.com/docs/en/build-with-claude/thinking-steering-and-cost#pricing) (on the [Steering thinking](https://platform.claude.com/docs/en/build-with-claude/thinking-steering-and-cost) page) ## Migrating to adaptive thinking If your model supports only extended thinking (Claude Sonnet 4.5, Claude Opus 4.5, Claude Haiku 4.5, and earlier Claude 4 models), no action is needed now: adaptive thinking is not available there, and `type: "adaptive"` [returns a 400 error](https://platform.claude.com/docs/en/build-with-claude/thinking-troubleshooting#error-thinking-type-adaptive). Keep `budget_tokens` until you move to a model that supports adaptive thinking, then apply the mapping that follows. You need to migrate off `type: "enabled"` if: * You use Claude Opus 4.6 or Claude Sonnet 4.6, where `budget_tokens` is deprecated. * You are moving to Claude Opus 4.7, Claude Opus 4.8, Claude Opus 5, Claude Sonnet 5, Claude Fable 5, or Claude Mythos 5, where `type: "enabled"` returns a 400 error. The mapping is small: remove `budget_tokens`, set `thinking: {type: "adaptive"}`, and control reasoning depth with `output_config: {effort: ...}` instead of a token budget. ```json { "model": "claude-sonnet-4-6", "max_tokens": 16000, "thinking": { "type": "enabled", "budget_tokens": 10000 } } ``` becomes: ```json { "model": "claude-sonnet-4-6", "max_tokens": 16000, "thinking": { "type": "adaptive" }, "output_config": { "effort": "high" } } ``` `effort: "high"` matches the API default; it appears here only to show where the depth control now lives, and omitting it produces identical behavior. Expect a behavioral difference, not just a syntax change. With a fixed budget, Claude thinks on every request. With adaptive thinking, Claude decides whether and how much to think on each request, and at lower [effort](https://platform.claude.com/docs/en/build-with-claude/effort) settings it may skip thinking entirely on easy inputs. You can also remove the `interleaved-thinking-2025-05-14` beta header after migrating: adaptive thinking interleaves automatically, and the Claude API ignores the header on these models. Thinking block preservation changes too: Claude Opus 4.5 and models numbered 4.6 and higher keep prior turns' thinking blocks in context and bill them as input, where Claude Sonnet 4.5, Claude Haiku 4.5, and earlier models stripped them; see [thinking block preservation by model](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-block-preservation-by-model). Switching modes is a thinking-configuration change, so the first request after the switch invalidates cache breakpoints, as described in [Prompt caching in manual mode](https://platform.claude.com/docs/en/build-with-claude/extended-thinking#extended-thinking-with-prompt-caching). For full guidance, see [adaptive thinking](https://platform.claude.com/docs/en/build-with-claude/thinking), [effort](https://platform.claude.com/docs/en/build-with-claude/effort), and the [model migration guide](https://platform.claude.com/docs/en/about-claude/models/migration-guide). ## Next steps Learn how thinking works: blocks, display, streaming, and tool use. Let Claude decide when and how much to think on each request. Preserve thinking blocks and manage thinking across tool calls and turns. --- title: Steering thinking url: https://platform.claude.com/docs/en/build-with-claude/thinking-steering-and-cost description: Steer how often and how deeply Claude thinks with effort levels, system prompt guidance, and per-message steering, and understand thinking's cost and pricing. --- For how zero data retention (ZDR) applies to this feature, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention). Claude's thinking is adaptive: the model evaluates each request and decides for itself whether to think and how much. You set an intent, optionally specify the effort, and the model allocates reasoning where it judges reasoning will help. This makes thinking a strong fit for workloads that mix trivial and complex requests, and for long-horizon agentic workflows where the right amount of reasoning varies from step to step. For how to turn thinking on, how to read thinking output, and [thinking output on Claude Fable 5 and Claude Mythos 5](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-output-on-claude-fable-5-and-claude-mythos-5), see the [Thinking](https://platform.claude.com/docs/en/build-with-claude/thinking) overview. This page covers how Claude decides when to think, how to steer that decision, and the caching, cost, and pricing mechanics that follow from it. ## How Claude decides when to think Thinking is optional for the model. On each request, Claude weighs the complexity of the input and decides whether deeper reasoning would improve the answer. A simple factual question may get a direct response with no thinking block at all; a multistep math problem or a tricky debugging task triggers deeper reasoning. The decision happens per request. The same conversation can contain turns with and without thinking, and a turn where Claude chose not to think contains no thinking block. Don't build application logic that assumes every assistant turn starts with one. The primary control over this decision is the [effort](https://platform.claude.com/docs/en/build-with-claude/effort) parameter, which acts as soft guidance for how willing Claude should be to think and how deeply; see [Effort levels](https://platform.claude.com/docs/en/build-with-claude/thinking-steering-and-cost#effort-levels) on this page for what each level does. If you want Claude to think less often, lower the effort level before reaching for prompt-based steering. Thinking also interleaves with tool use automatically: Claude can think between tool calls, reflecting on each tool result before deciding what to do next ([interleaved thinking](https://platform.claude.com/docs/en/build-with-claude/thinking#interleaved-thinking)). You don't need a beta header or any additional configuration for this. For the full picture of how the thinking configuration and the effort parameter interact, see [Thinking and effort](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-and-effort). ## Steering how often Claude thinks Whether Claude thinks on a given turn is promptable. Effort sets the overall posture, but you can also shape the decision directly with natural-language guidance, either globally in the system prompt or per message from the user turn. Use the two levers together in this order: 1. Set the effort level that matches your workload's default balance of quality and latency. 2. Add prompt guidance only if Claude's triggering still doesn't match your needs at that level. For broader prompting guidance with thinking, see [leverage thinking and interleaved thinking capabilities](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-prompting-best-practices#leverage-thinking-and-interleaved-thinking-capabilities). ### Effort levels Effort is the primary steering lever for thinking. Each level sets a different default for how often Claude thinks and how deeply: | Effort level | Thinking behavior | | ---------------- | ------------------------------------------------------------------------------------ | | `max` | Claude always thinks with no constraints on thinking depth. | | `xhigh` | Claude always thinks deeply with extended exploration. | | `high` (default) | Claude almost always thinks. Provides deep reasoning on complex tasks. | | `medium` | Claude uses moderate thinking. May skip thinking for simple queries. | | `low` | Claude minimizes thinking. Skips thinking for simple tasks where speed matters most. | This table describes how each level changes thinking behavior. For guidance on which level to choose for a given workload, including per-model recommendations, see [When to adjust the effort parameter](https://platform.claude.com/docs/en/build-with-claude/effort#when-to-adjust-the-effort-parameter) on the effort page. Effort is set at `output_config.effort`, not inside the `thinking` object; for full per-language examples, see [Effort](https://platform.claude.com/docs/en/build-with-claude/effort#basic-usage). ```json { "model": "claude-opus-4-8", "max_tokens": 4096, "output_config": { "effort": "medium" }, "messages": [{ "role": "user", "content": "..." }] } ``` Level availability varies by model; the [effort availability table](https://platform.claude.com/docs/en/build-with-claude/effort#effort-levels) on the effort page is the authority for which levels each model supports. ### System prompt guidance System prompt guidance shifts Claude's thinking threshold for every request in the conversation. If Claude is thinking more often than your workload needs, add guidance like this to your system prompt: ```text wrap Extended thinking adds latency and should only be used when it will meaningfully improve answer quality, typically for problems that require multistep reasoning. When in doubt, respond directly. ``` To encourage thinking instead, use a phrase like: ```text wrap This task involves multistep reasoning. Think carefully before responding. ``` Steering effectiveness can be sensitive to exact wording. If one phrasing doesn't produce the behavior you want, try a more direct variant. ### Per-message steering You can also steer thinking on a per-message basis from the user turn, independently of the system prompt. Appending `"Please think hard before responding."` to a user message encourages Claude to think on that turn; `"Answer directly without deliberating."` suppresses it. Per-message steering is useful when only some requests in a conversation warrant extended reasoning. An agent harness, for example, can append the encouraging phrase on planning steps and the suppressing phrase on routine confirmations, without touching the system prompt or changing any request parameters between turns. ### Verify steering on your workload Prompt-based steering changes model behavior, so treat it like any other prompt change: measure before you ship. Run a representative sample of your traffic with and without the guidance, and compare how often thinking triggers (the presence of thinking blocks in responses), output token usage, latency, and answer quality on the cases that matter to you. Steering Claude to think less often may reduce quality on tasks that benefit from reasoning. Lowering the [effort](https://platform.claude.com/docs/en/build-with-claude/effort) level is usually the better first lever, since it is a calibrated control rather than a wording-sensitive instruction. Measure the impact on your specific workloads before deploying prompt-based tuning to production. ## Mechanics Three mechanics follow from Claude managing its own thinking: turn validation, prompt caching, and how you bound cost. ### Turn validation Assistant turns don't need to start with a thinking block. (Models using a legacy manual thinking budget enforce that the final assistant turn of a thinking-enabled request begins with one; see [Turn structure in manual mode](https://platform.claude.com/docs/en/build-with-claude/extended-thinking#turn-structure-in-manual-mode).) For multi-turn applications, this means you can pass back conversation history in whatever shape you have it: * Assistant turns where Claude chose not to think are valid history as-is. * You can resume a conversation that began without thinking, or that used a different thinking configuration, without rewriting its history. * History assembled from mixed sources doesn't need thinking blocks reinserted at the start of each assistant turn to pass validation. The relaxation is about validation, not about what you should send. When you have thinking blocks, pass them back unmodified, particularly during tool use, where they carry the reasoning behind Claude's tool calls. See the [Thinking](https://platform.claude.com/docs/en/build-with-claude/thinking) overview for the full rules. ### Prompt caching Consecutive requests that keep the same thinking configuration and effort level preserve prompt caching; see [Thinking and prompt caching](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-and-prompt-caching) for the full rules. The resolved effort value is rendered into the prompt, so changing it between requests invalidates cache breakpoints, just as changing the legacy [`budget_tokens`](https://platform.claude.com/docs/en/build-with-claude/extended-thinking#extended-thinking-with-prompt-caching) parameter does on models that use it. Setting `effort` explicitly to the model's default is equivalent to omitting it and does not break the cache. The practical consequence: pick a thinking configuration and an effort level per conversation and keep them. If some turns need more or less thinking, steer with [per-message prompting](https://platform.claude.com/docs/en/build-with-claude/thinking-steering-and-cost#tuning-thinking-behavior): guidance appended to the newest user message leaves earlier cache breakpoints intact, where a configuration or effort change does not. The following example demonstrates the invalidation with a multi-turn script you can run yourself: This workflow doesn't translate well to a one-off shell command. See the SDK tabs for the multi-turn pattern; per-turn HTTP requests follow the examples on the [Prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) page. This workflow doesn't translate well to a one-off shell command. See the SDK tabs for the multi-turn pattern; per-turn CLI invocations follow the examples on the [Prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) page. ```python import requests client = Anthropic() def fetch_article_content(url): text = requests.get(url).text lines = (line.strip() for line in text.splitlines()) return "\n".join(line for line in lines if line) # Fetch the content of the article book_url = "https://www.gutenberg.org/cache/epub/1342/pg1342.txt" book_content = fetch_article_content(book_url) # Use just enough text for caching (first few chapters) LARGE_TEXT = book_content[:10000] # No system prompt - caching in messages instead MESSAGES = [ { "role": "user", "content": [ { "type": "text", "text": LARGE_TEXT, "cache_control": {"type": "ephemeral"}, }, {"type": "text", "text": "Analyze the tone of this passage."}, ], } ] # First request - establish cache print("First request - establishing cache") response1 = client.messages.create( model="claude-opus-4-8", max_tokens=16000, thinking={"type": "adaptive"}, messages=MESSAGES, ) print(f"First response usage: {response1.usage}") MESSAGES.append({"role": "assistant", "content": response1.content}) MESSAGES.append({"role": "user", "content": "Analyze the characters in this passage."}) # Second request - same configuration (cache hit expected) print("\nSecond request - same configuration (cache hit expected)") response2 = client.messages.create( model="claude-opus-4-8", max_tokens=16000, thinking={"type": "adaptive"}, messages=MESSAGES, ) print(f"Second response usage: {response2.usage}") MESSAGES.append({"role": "assistant", "content": response2.content}) MESSAGES.append({"role": "user", "content": "Analyze the setting in this passage."}) # Third request - different effort level (cache miss expected) print("\nThird request - different effort level (cache miss expected)") response3 = client.messages.create( model="claude-opus-4-8", max_tokens=16000, thinking={"type": "adaptive"}, output_config={"effort": "medium"}, messages=MESSAGES, ) print(f"Third response usage: {response3.usage}") ``` ```typescript const client = new Anthropic(); async function fetchArticleContent(url: string): Promise { const response = await fetch(url); const text = await response.text(); const lines = text.split("\n").map((line) => line.trim()); return lines.filter((line) => line).join("\n"); } const bookUrl = "https://www.gutenberg.org/cache/epub/1342/pg1342.txt"; const bookContent = await fetchArticleContent(bookUrl); const LARGE_TEXT = bookContent.substring(0, 10000); // No system prompt - caching in messages instead const messages: Anthropic.MessageParam[] = [ { role: "user", content: [ { type: "text", text: LARGE_TEXT, cache_control: { type: "ephemeral" } }, { type: "text", text: "Analyze the tone of this passage." } ] } ]; // First request - establish cache console.log("First request - establishing cache"); const response1 = await client.messages.create({ model: "claude-opus-4-8", max_tokens: 16000, thinking: { type: "adaptive" }, messages }); console.log("First response usage: ", response1.usage); messages.push( { role: "assistant", content: response1.content }, { role: "user", content: "Analyze the characters in this passage." } ); // Second request - same configuration (cache hit expected) console.log("\nSecond request - same configuration (cache hit expected)"); const response2 = await client.messages.create({ model: "claude-opus-4-8", max_tokens: 16000, thinking: { type: "adaptive" }, messages }); console.log("Second response usage: ", response2.usage); messages.push( { role: "assistant", content: response2.content }, { role: "user", content: "Analyze the setting in this passage." } ); // Third request - different effort level (cache miss expected) console.log("\nThird request - different effort level (cache miss expected)"); const response3 = await client.messages.create({ model: "claude-opus-4-8", max_tokens: 16000, thinking: { type: "adaptive" }, output_config: { effort: "medium" }, messages }); console.log("Third response usage: ", response3.usage); ``` ```csharp AnthropicClient client = new(); string bookUrl = "https://www.gutenberg.org/cache/epub/1342/pg1342.txt"; string bookContent = await FetchArticleContent(bookUrl); string largeText = bookContent.Substring(0, Math.Min(10000, bookContent.Length)); Console.WriteLine("First request - establishing cache"); var parameters1 = new MessageCreateParams { Model = Model.ClaudeOpus4_8, MaxTokens = 16000, Thinking = new ThinkingConfigAdaptive(), Messages = [ new() { Role = Role.User, Content = new MessageParamContent(new List { new ContentBlockParam(new TextBlockParam() { Text = largeText, CacheControl = new CacheControlEphemeral(), }), new ContentBlockParam(new TextBlockParam() { Text = "Analyze the tone of this passage." }), }) } ] }; var response1 = await client.Messages.Create(parameters1); Console.WriteLine($"First response usage: {response1.Usage}"); Console.WriteLine("\nSecond request - same configuration (cache hit expected)"); var parameters2 = new MessageCreateParams { Model = Model.ClaudeOpus4_8, MaxTokens = 16000, Thinking = new ThinkingConfigAdaptive(), Messages = [ new() { Role = Role.User, Content = new MessageParamContent(new List { new ContentBlockParam(new TextBlockParam() { Text = largeText, CacheControl = new CacheControlEphemeral(), }), new ContentBlockParam(new TextBlockParam() { Text = "Analyze the tone of this passage." }), }) }, new() { Role = Role.Assistant, Content = response1.Content.Select(block => new ContentBlockParam(block.Json)).ToList() }, new() { Role = Role.User, Content = "Analyze the characters in this passage." } ] }; var response2 = await client.Messages.Create(parameters2); Console.WriteLine($"Second response usage: {response2.Usage}"); Console.WriteLine("\nThird request - different effort level (cache miss expected)"); var parameters3 = new MessageCreateParams { Model = Model.ClaudeOpus4_8, MaxTokens = 16000, Thinking = new ThinkingConfigAdaptive(), OutputConfig = new OutputConfig { Effort = Effort.Medium }, Messages = [ new() { Role = Role.User, Content = new MessageParamContent(new List { new ContentBlockParam(new TextBlockParam() { Text = largeText, CacheControl = new CacheControlEphemeral(), }), new ContentBlockParam(new TextBlockParam() { Text = "Analyze the tone of this passage." }), }) }, new() { Role = Role.Assistant, Content = response1.Content.Select(block => new ContentBlockParam(block.Json)).ToList() }, new() { Role = Role.User, Content = "Analyze the characters in this passage." }, new() { Role = Role.Assistant, Content = response2.Content.Select(block => new ContentBlockParam(block.Json)).ToList() }, new() { Role = Role.User, Content = "Analyze the setting in this passage." } ] }; var response3 = await client.Messages.Create(parameters3); Console.WriteLine($"Third response usage: {response3.Usage}"); static async Task FetchArticleContent(string url) { using HttpClient httpClient = new(); string content = await httpClient.GetStringAsync(url); return content; } ``` ```go client := anthropic.NewClient() bookURL := "https://www.gutenberg.org/cache/epub/1342/pg1342.txt" bookContent, err := fetchArticleContent(bookURL) if err != nil { log.Fatal(err) } largeText := bookContent if len(largeText) > 10000 { largeText = largeText[:10000] } // No system prompt - caching in messages instead messages := []anthropic.MessageParam{ anthropic.NewUserMessage( anthropic.ContentBlockParamUnion{OfText: &anthropic.TextBlockParam{ Text: largeText, CacheControl: anthropic.NewCacheControlEphemeralParam(), }}, anthropic.NewTextBlock("Analyze the tone of this passage."), ), } // First request - establish cache fmt.Println("First request - establishing cache") response1, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus4_8, MaxTokens: 16000, Thinking: anthropic.ThinkingConfigParamUnion{ OfAdaptive: &anthropic.ThinkingConfigAdaptiveParam{}, }, Messages: messages, }) if err != nil { log.Fatal(err) } fmt.Printf("First response usage: %s\n", response1.Usage.RawJSON()) messages = append(messages, response1.ToParam()) messages = append(messages, anthropic.NewUserMessage(anthropic.NewTextBlock("Analyze the characters in this passage."))) // Second request - same configuration (cache hit expected) fmt.Println("\nSecond request - same configuration (cache hit expected)") response2, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus4_8, MaxTokens: 16000, Thinking: anthropic.ThinkingConfigParamUnion{ OfAdaptive: &anthropic.ThinkingConfigAdaptiveParam{}, }, Messages: messages, }) if err != nil { log.Fatal(err) } fmt.Printf("Second response usage: %s\n", response2.Usage.RawJSON()) messages = append(messages, response2.ToParam()) messages = append(messages, anthropic.NewUserMessage(anthropic.NewTextBlock("Analyze the setting in this passage."))) // Third request - different effort level (cache miss expected) fmt.Println("\nThird request - different effort level (cache miss expected)") response3, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus4_8, MaxTokens: 16000, Thinking: anthropic.ThinkingConfigParamUnion{ OfAdaptive: &anthropic.ThinkingConfigAdaptiveParam{}, }, OutputConfig: anthropic.OutputConfigParam{ Effort: anthropic.OutputConfigEffortMedium, }, Messages: messages, }) if err != nil { log.Fatal(err) } fmt.Printf("Third response usage: %s\n", response3.Usage.RawJSON()) ``` ```java import com.anthropic.models.messages.CacheControlEphemeral; // ... void main() throws Exception { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); String bookUrl = "https://www.gutenberg.org/cache/epub/1342/pg1342.txt"; String bookContent = fetchArticleContent(bookUrl); String largeText = bookContent.substring(0, Math.min(10000, bookContent.length())); // First request - establishing cache IO.println("First request - establishing cache"); MessageCreateParams params1 = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_4_8) .maxTokens(16000L) .thinking(ThinkingConfigAdaptive.builder().build()) .addUserMessageOfBlockParams(List.of( ContentBlockParam.ofText(TextBlockParam.builder() .text(largeText) .cacheControl(CacheControlEphemeral.builder().build()) .build()), ContentBlockParam.ofText(TextBlockParam.builder() .text("Analyze the tone of this passage.") .build()) )) .build(); Message response1 = client.messages().create(params1); IO.println("First response usage: " + response1.usage()); // Second request - same configuration (cache hit expected) IO.println("\nSecond request - same configuration (cache hit expected)"); MessageCreateParams params2 = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_4_8) .maxTokens(16000L) .thinking(ThinkingConfigAdaptive.builder().build()) .addUserMessageOfBlockParams(List.of( ContentBlockParam.ofText(TextBlockParam.builder() .text(largeText) .cacheControl(CacheControlEphemeral.builder().build()) .build()), ContentBlockParam.ofText(TextBlockParam.builder() .text("Analyze the tone of this passage.") .build()) )) .addAssistantMessageOfBlockParams(response1.content().stream() .map(block -> block.toParam()) .collect(java.util.stream.Collectors.toList())) .addUserMessage("Analyze the characters in this passage.") .build(); Message response2 = client.messages().create(params2); IO.println("Second response usage: " + response2.usage()); // Third request - different effort level (cache miss expected) IO.println("\nThird request - different effort level (cache miss expected)"); MessageCreateParams params3 = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_4_8) .maxTokens(16000L) .thinking(ThinkingConfigAdaptive.builder().build()) .outputConfig(OutputConfig.builder() .effort(OutputConfig.Effort.MEDIUM) .build()) .addUserMessageOfBlockParams(List.of( ContentBlockParam.ofText(TextBlockParam.builder() .text(largeText) .cacheControl(CacheControlEphemeral.builder().build()) .build()), ContentBlockParam.ofText(TextBlockParam.builder() .text("Analyze the tone of this passage.") .build()) )) .addAssistantMessageOfBlockParams(response1.content().stream() .map(block -> block.toParam()) .collect(java.util.stream.Collectors.toList())) .addUserMessage("Analyze the characters in this passage.") .addAssistantMessageOfBlockParams(response2.content().stream() .map(block -> block.toParam()) .collect(java.util.stream.Collectors.toList())) .addUserMessage("Analyze the setting in this passage.") .build(); Message response3 = client.messages().create(params3); IO.println("Third response usage: " + response3.usage()); } String fetchArticleContent(String url) throws Exception { HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); return response.body(); } ``` ```php function fetchArticleContent($url) { $content = file_get_contents($url); $lines = explode("\n", $content); $cleanedLines = array_filter(array_map('trim', $lines)); return implode("\n", $cleanedLines); } $client = new Client(); $bookUrl = "https://www.gutenberg.org/cache/epub/1342/pg1342.txt"; $bookContent = fetchArticleContent($bookUrl); $largeText = substr($bookContent, 0, 10000); echo "First request - establishing cache\n"; $response1 = $client->messages->create( maxTokens: 16000, messages: [[ 'role' => 'user', 'content' => [ [ 'type' => 'text', 'text' => $largeText, 'cache_control' => ['type' => 'ephemeral'] ], [ 'type' => 'text', 'text' => 'Analyze the tone of this passage.' ] ] ]], model: 'claude-opus-4-8', thinking: ['type' => 'adaptive'], ); echo "First response usage: " . json_encode($response1->usage) . "\n"; echo "\nSecond request - same configuration (cache hit expected)\n"; $response2 = $client->messages->create( maxTokens: 16000, messages: [ [ 'role' => 'user', 'content' => [ [ 'type' => 'text', 'text' => $largeText, 'cache_control' => ['type' => 'ephemeral'] ], [ 'type' => 'text', 'text' => 'Analyze the tone of this passage.' ] ] ], [ 'role' => 'assistant', 'content' => $response1->content ], [ 'role' => 'user', 'content' => 'Analyze the characters in this passage.' ] ], model: 'claude-opus-4-8', thinking: ['type' => 'adaptive'], ); echo "Second response usage: " . json_encode($response2->usage) . "\n"; echo "\nThird request - different effort level (cache miss expected)\n"; $response3 = $client->messages->create( maxTokens: 16000, messages: [ [ 'role' => 'user', 'content' => [ [ 'type' => 'text', 'text' => $largeText, 'cache_control' => ['type' => 'ephemeral'] ], [ 'type' => 'text', 'text' => 'Analyze the tone of this passage.' ] ] ], [ 'role' => 'assistant', 'content' => $response1->content ], [ 'role' => 'user', 'content' => 'Analyze the characters in this passage.' ], [ 'role' => 'assistant', 'content' => $response2->content ], [ 'role' => 'user', 'content' => 'Analyze the setting in this passage.' ] ], model: 'claude-opus-4-8', thinking: ['type' => 'adaptive'], outputConfig: ['effort' => 'medium'], ); echo "Third response usage: " . json_encode($response3->usage) . "\n"; ``` ```ruby require "net/http" require "uri" def fetch_article_content(url) uri = URI.parse(url) response = Net::HTTP.get_response(uri) text = response.body lines = text.split("\n").map(&:strip) lines.reject(&:empty?).join("\n") end client = Anthropic::Client.new book_url = "https://www.gutenberg.org/cache/epub/1342/pg1342.txt" book_content = fetch_article_content(book_url) large_text = book_content[0...10000] puts "First request - establishing cache" response1 = client.messages.create( model: "claude-opus-4-8", max_tokens: 16000, thinking: { type: "adaptive" }, messages: [{ role: "user", content: [ { type: "text", text: large_text, cache_control: { type: "ephemeral" } }, { type: "text", text: "Analyze the tone of this passage." } ] }] ) puts "First response usage: #{response1.usage}" puts "\nSecond request - same configuration (cache hit expected)" response2 = client.messages.create( model: "claude-opus-4-8", max_tokens: 16000, thinking: { type: "adaptive" }, messages: [ { role: "user", content: [ { type: "text", text: large_text, cache_control: { type: "ephemeral" } }, { type: "text", text: "Analyze the tone of this passage." } ] }, { role: "assistant", content: response1.content }, { role: "user", content: "Analyze the characters in this passage." } ] ) puts "Second response usage: #{response2.usage}" puts "\nThird request - different effort level (cache miss expected)" response3 = client.messages.create( model: "claude-opus-4-8", max_tokens: 16000, thinking: { type: "adaptive" }, output_config: { effort: "medium" }, messages: [ { role: "user", content: [ { type: "text", text: large_text, cache_control: { type: "ephemeral" } }, { type: "text", text: "Analyze the tone of this passage." } ] }, { role: "assistant", content: response1.content }, { role: "user", content: "Analyze the characters in this passage." }, { role: "assistant", content: response2.content }, { role: "user", content: "Analyze the setting in this passage." } ] ) puts "Third response usage: #{response3.usage}" ``` Here is the output of the script (you may see slightly different numbers): ```text Output wrap First request - establishing cache First response usage: { cache_creation_input_tokens: 3546, cache_read_input_tokens: 0, input_tokens: 15, output_tokens: 1033 } Second request - same configuration (cache hit expected) Second response usage: { cache_creation_input_tokens: 0, cache_read_input_tokens: 3546, input_tokens: 1062, output_tokens: 1630 } Third request - different effort level (cache miss expected) Third response usage: { cache_creation_input_tokens: 3546, cache_read_input_tokens: 0, input_tokens: 2706, output_tokens: 1468 } ``` With the cache breakpoint in the messages array, changing effort from the default `high` to `medium` invalidates it: the third request shows `cache_creation_input_tokens=3546` and `cache_read_input_tokens=0` where the second showed a full cache read. ### Cost control You don't set a thinking token budget. Two controls bound cost: * `max_tokens` is a hard cap on total output for the request, thinking and response text combined. Claude never generates past it. In a tool-use loop, each request in the turn has its own `max_tokens`, so it doesn't bound the whole turn's spend. * `effort` is soft guidance on how much of that output Claude allocates to thinking. It shapes behavior but doesn't guarantee a token count. Because thinking counts toward `max_tokens`, set it high enough to leave room for both the reasoning and the answer. A `max_tokens` sized for a response with no thinking is often too small once Claude starts thinking on hard requests. At `high` effort and above, Claude may think extensively and is more likely to exhaust the budget. If you see [`stop_reason: "max_tokens"`](https://platform.claude.com/docs/en/build-with-claude/thinking-troubleshooting#stopped-at-max-tokens) in responses, you have two remedies: * Raise `max_tokens` to give the model more room for thinking plus the answer. * Lower the effort level so Claude thinks less and leaves more of the budget for response text. Which one is right depends on whether the truncated responses needed the reasoning. If quality on those requests matters, raise the cap; if they were over-thought, lower the effort. ## Pricing Thinking incurs charges for: * Tokens Claude uses while thinking (billed as output tokens) * Thinking blocks from prior assistant turns that remain in context, per the [preservation default](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-block-preservation-by-model): all turns by default on keep-all models, only the last turn elsewhere (billed as input tokens) * Standard text output tokens When thinking is active, a specialized system prompt is automatically included to support this feature. What you're billed for is the same regardless of the `display` setting; only what you see changes: | | `display: "summarized"` | `display: "omitted"` | | --------------------------- | ---------------------------------------------------- | ---------------------------------------------------- | | **Input tokens** | Tokens in your original request | Same as summarized | | **Output tokens (billed)** | The full thinking tokens Claude generated internally | Same as summarized | | **Output tokens (visible)** | The summarized thinking text | Zero thinking tokens (the `thinking` field is empty) | | **Summary generation** | No charge | Not applicable | The billed output token count does **not** match the visible token count in the response. You are billed for the full thinking process, not the thinking content visible in the response. To see how many billed output tokens were spent on internal reasoning, read `usage.output_tokens_details.thinking_tokens` in the response. This value reflects the raw reasoning the model generated (not the summarized text returned in the body) and is always less than or equal to `output_tokens`. Subtract it from `output_tokens` to approximate the non-reasoning portion of the output. When streaming, this breakdown appears only on the final `message_delta` event. ```json { "usage": { "input_tokens": 25, "output_tokens": 348, "output_tokens_details": { "thinking_tokens": 312 } } } ``` `output_tokens` remains the inclusive, authoritative total used for billing. `output_tokens_details` is a read-only breakdown for observability. For complete pricing information including base rates, cache writes, cache hits, and output tokens, see [Pricing](https://platform.claude.com/docs/en/about-claude/pricing). ## Next steps Turn thinking on, read thinking output, and check per-model support. Preserve thinking blocks across tool calls and manage thinking in multi-turn conversations. Control how much thinking and output Claude allocates per request. --- title: Thinking url: https://platform.claude.com/docs/en/build-with-claude/thinking description: "Understand how Claude's thinking works: turn it on, read thinking output, steer thinking depth with effort, and use thinking with tools, caching, and streaming." --- For how zero data retention (ZDR) applies to this feature, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention). A model that answers in a single pass has to get everything right on the first try: no scratch work, no checking, no changing course halfway through. For a proof, a tricky bug, or a long agentic task, the first approach is often not the best one. Thinking removes that constraint. When thinking is active, Claude works through the problem in its own words before answering: it restates what is being asked, tries approaches, checks intermediate results, and abandons paths that do not hold up. That reasoning arrives in `thinking` content blocks ahead of the response, and Claude draws on it to produce the final answer. This is why thinking improves performance on complex tasks like math, coding, analysis, and long-running agentic work, where the quality of the answer depends on intermediate work that would otherwise be compressed into the response itself or skipped. Thinking has a cost: the tokens Claude spends reasoning are billed as output tokens, even when the thinking text isn't returned to you, and they count toward `max_tokens` alongside the response text. This page covers how thinking behaves across the API surface: turning it on, reading its output, and managing its interactions with tools, streaming, caching, and the context window. ## How thinking works ![Diagram of how thinking works: Claude evaluates the request and decides whether to think; with tool use, thinking can recur between tool calls; one response returns thinking blocks, then text blocks](https://platform.claude.com/docs/images/how-thinking-works.svg) Whether Claude thinks on a given request, and how deeply, depends on your thinking configuration and the complexity of the request. Here is what thinking looks like in a response: one or more `thinking` content blocks arrive before the `text` blocks. The thinking block is still generated content, like the `text` block that follows it, but it is separated from the canonical response. Each thinking block also carries a `signature` field, an encrypted copy of the full reasoning that you pass back unchanged in multi-turn and tool-use conversations (see [Thinking encryption](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-encryption)): ```json { "content": [ { "type": "thinking", "thinking": "Let me break this down. The question has two parts, so I'll start with the simpler one and use its result to constrain the second...", "signature": "WaUjzkypQ2mUEVM36O2Txu...." }, { "type": "text", "text": "Based on my analysis..." } ] } ``` You don't always see this text, and what you see is never the raw chain of thought: the text in a thinking block is a [summary of Claude's reasoning](https://platform.claude.com/docs/en/build-with-claude/thinking#summarized-thinking). The `display` field on the thinking configuration controls whether that summary is returned at all: `"summarized"` returns it, while `"omitted"`, the default on the newest models, returns thinking blocks with an empty `thinking` field. Either way the block is billed the same and passed back the same in multi-turn conversations. See [Controlling thinking display](https://platform.claude.com/docs/en/build-with-claude/thinking#controlling-thinking-display) for per-model defaults and details. If Claude uses tools, thinking can also appear between tool calls. See [Thinking with tool use](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-with-tool-use). For the full response format, see the [Messages API reference](https://platform.claude.com/docs/en/api/messages/create). ## Configuring thinking On current models, thinking is on by default or one parameter away. Which configuration each model accepts, and what it defaults to, is listed in the [per-model configuration table](https://platform.claude.com/docs/en/build-with-claude/thinking-troubleshooting#supported-models) on the Troubleshooting page. On Claude Opus 5, Claude Sonnet 5, Claude Fable 5, Claude Mythos 5, and Claude Mythos Preview, thinking is already on: no configuration needed. The first thing most developers need on these models is to see the thinking text, because `display` defaults to `"omitted"` there. Opt in with `thinking: {"type": "adaptive", "display": "summarized"}`, which is exactly the following request with the [model string](https://platform.claude.com/docs/en/about-claude/models/overview) swapped. On Claude Opus 4.8, Claude Opus 4.7, Claude Opus 4.6, and Claude Sonnet 4.6, thinking is off until you set `thinking: {type: "adaptive"}`, which lets Claude decide when and how deeply to think based on the request. The following examples do that, set `display: "summarized"` so the thinking text is visible, and use a roomy `max_tokens`: ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-4-8", "max_tokens": 16000, "thinking": { "type": "adaptive", "display": "summarized" }, "messages": [ { "role": "user", "content": "What is the greatest common divisor of 1071 and 462?" } ] }' ``` ```bash CLI ant messages create \ --model claude-opus-4-8 \ --max-tokens 16000 \ --thinking '{type: adaptive, display: summarized}' \ --message '{role: user, content: "What is the greatest common divisor of 1071 and 462?"}' \ --transform content \ --format yaml ``` ```python Python client = anthropic.Anthropic() response = client.messages.create( model="claude-opus-4-8", max_tokens=16000, thinking={"type": "adaptive", "display": "summarized"}, messages=[ { "role": "user", "content": "What is the greatest common divisor of 1071 and 462?", } ], ) for block in response.content: if block.type == "thinking": print(f"\nThinking: {block.thinking}") elif block.type == "text": print(f"\nResponse: {block.text}") ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.messages.create({ model: "claude-opus-4-8", max_tokens: 16000, thinking: { type: "adaptive", display: "summarized" }, messages: [ { role: "user", content: "What is the greatest common divisor of 1071 and 462?" } ] }); for (const block of response.content) { if (block.type === "thinking") { console.log(`\nThinking: ${block.thinking}`); } else if (block.type === "text") { console.log(`\nResponse: ${block.text}`); } } ``` ```csharp C# AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus4_8, MaxTokens = 16000, Thinking = new ThinkingConfigAdaptive { Display = Display.Summarized }, Messages = [ new() { Role = Role.User, Content = "What is the greatest common divisor of 1071 and 462?" } ] }; var message = await client.Messages.Create(parameters); foreach (var block in message.Content) { if (block.TryPickThinking(out ThinkingBlock? thinking)) { Console.WriteLine($"\nThinking: {thinking.Thinking}"); } else if (block.TryPickText(out TextBlock? text)) { Console.WriteLine($"\nResponse: {text.Text}"); } } ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus4_8, MaxTokens: 16000, Thinking: anthropic.ThinkingConfigParamUnion{ OfAdaptive: &anthropic.ThinkingConfigAdaptiveParam{ Display: anthropic.ThinkingConfigAdaptiveDisplaySummarized, }, }, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("What is the greatest common divisor of 1071 and 462?")), }, }) if err != nil { log.Fatal(err) } for _, block := range response.Content { switch v := block.AsAny().(type) { case anthropic.ThinkingBlock: fmt.Printf("\nThinking: %s", v.Thinking) case anthropic.TextBlock: fmt.Printf("\nResponse: %s", v.Text) } } ``` ```java Java import com.anthropic.models.messages.ThinkingConfigAdaptive; void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_4_8) .maxTokens(16000L) .thinking(ThinkingConfigAdaptive.builder() .display(ThinkingConfigAdaptive.Display.SUMMARIZED) .build()) .addUserMessage("What is the greatest common divisor of 1071 and 462?") .build(); Message response = client.messages().create(params); response.content().forEach(block -> { block.thinking().ifPresent(thinkingBlock -> IO.println("\nThinking: " + thinkingBlock.thinking()) ); block.text().ifPresent(textBlock -> IO.println("\nResponse: " + textBlock.text()) ); }); } ``` ```php PHP $client = new Client(); $message = $client->messages->create( maxTokens: 16000, messages: [ [ 'role' => 'user', 'content' => 'What is the greatest common divisor of 1071 and 462?' ] ], model: 'claude-opus-4-8', thinking: ['type' => 'adaptive', 'display' => 'summarized'], ); foreach ($message->content as $block) { if ($block->type === 'thinking') { echo "\nThinking: " . $block->thinking; } elseif ($block->type === 'text') { echo "\nResponse: " . $block->text; } } ``` ```ruby Ruby client = Anthropic::Client.new message = client.messages.create( model: "claude-opus-4-8", max_tokens: 16000, thinking: { type: "adaptive", display: "summarized" }, messages: [ { role: "user", content: "What is the greatest common divisor of 1071 and 462?" } ] ) message.content.each do |block| case block.type when :thinking puts "\nThinking: #{block.thinking}" when :text puts "\nResponse: #{block.text}" end end ``` Running the example prints the summarized thinking, then the answer: ```text Output wrap Thinking: Use Euclidean algorithm. 1071 = 2*462 + 147 462 = 3*147 + 21 147 = 7*21 + 0 GCD = 21 Response: ## Finding GCD of 1071 and 462 I'll use the **Euclidean algorithm**, repeatedly dividing and taking remainders... ``` Thinking tokens count toward `max_tokens`, so set it high enough to leave room for both the thinking and the response text. See [Cost control](https://platform.claude.com/docs/en/build-with-claude/thinking-steering-and-cost#cost-control) on the steering page and [Thinking and the context window](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-and-the-context-window). ### Turning thinking off On Claude Sonnet 5, where thinking is on by default, you can turn it off: ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-sonnet-5", "max_tokens": 4096, "thinking": {"type": "disabled"}, "messages": [ { "role": "user", "content": "Summarize this article in one sentence." } ] }' ``` ```bash CLI ant messages create \ --model claude-sonnet-5 \ --max-tokens 4096 \ --thinking '{type: disabled}' \ --message '{role: user, content: "Summarize this article in one sentence."}' ``` ```python Python client = anthropic.Anthropic() response = client.messages.create( model="claude-sonnet-5", max_tokens=4096, thinking={"type": "disabled"}, messages=[{"role": "user", "content": "Summarize this article in one sentence."}], ) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.messages.create({ model: "claude-sonnet-5", max_tokens: 4096, thinking: { type: "disabled" }, messages: [{ role: "user", content: "Summarize this article in one sentence." }] }); ``` ```csharp C# AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Model.ClaudeSonnet5, MaxTokens = 4096, Thinking = new ThinkingConfigDisabled(), Messages = [ new() { Role = Role.User, Content = "Summarize this article in one sentence." } ] }; var message = await client.Messages.Create(parameters); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeSonnet5, MaxTokens: 4096, Thinking: anthropic.ThinkingConfigParamUnion{ OfDisabled: &anthropic.ThinkingConfigDisabledParam{}, }, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Summarize this article in one sentence.")), }, }) if err != nil { log.Fatal(err) } ``` ```java Java import com.anthropic.models.messages.ThinkingConfigDisabled; void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_SONNET_5) .maxTokens(4096L) .thinking(ThinkingConfigDisabled.builder().build()) .addUserMessage("Summarize this article in one sentence.") .build(); Message response = client.messages().create(params); } ``` ```php PHP $client = new Client(); $message = $client->messages->create( maxTokens: 4096, messages: [ [ 'role' => 'user', 'content' => 'Summarize this article in one sentence.' ] ], model: 'claude-sonnet-5', thinking: ['type' => 'disabled'], ); ``` ```ruby Ruby client = Anthropic::Client.new message = client.messages.create( model: "claude-sonnet-5", max_tokens: 4096, thinking: { type: "disabled" }, messages: [ { role: "user", content: "Summarize this article in one sentence." } ] ) ``` Claude Opus 5 also has thinking on by default and accepts `thinking: {type: "disabled"}` at [effort](https://platform.claude.com/docs/en/build-with-claude/effort) `high` or below. At `xhigh` or `max` effort, thinking cannot be turned off: requests that combine `thinking: {type: "disabled"}` with those effort levels return a 400 error. This restriction applies to Claude Opus 5 and later models and is enforced on each request. With thinking disabled, Claude Opus 5 can occasionally emit tool calls as plain text or include internal XML tags in its visible output. See [Running with thinking disabled](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/prompting-claude-opus-5#running-with-thinking-disabled) for prompting mitigations. Claude Fable 5, Claude Mythos 5, and Claude Mythos Preview reject `thinking: {type: "disabled"}`: thinking cannot be turned off on these models. If your model supports only extended thinking (see the [per-model configuration table](https://platform.claude.com/docs/en/build-with-claude/thinking-troubleshooting#supported-models)), configure it with `type: "enabled"` and a `budget_tokens` value instead. The [Extended thinking](https://platform.claude.com/docs/en/build-with-claude/extended-thinking) page covers that configuration. And if any thinking configuration comes back with a 400 error, [Troubleshooting thinking](https://platform.claude.com/docs/en/build-with-claude/thinking-troubleshooting) matches each error message to its fix. ## Reading thinking output ### Controlling thinking display The `display` field on the thinking configuration controls how thinking content is returned in API responses. `display` works in both modes: set it alongside `type: "adaptive"` or `type: "enabled"`. It accepts two values: * `"summarized"`: thinking blocks contain [summarized thinking](https://platform.claude.com/docs/en/build-with-claude/thinking#summarized-thinking) text, a readable summary of Claude's reasoning. This is the default on Claude Opus 4.6, Claude Sonnet 4.6, and earlier models. * `"omitted"`: thinking blocks are returned with an empty `thinking` field. The `signature` field still carries the encrypted full thinking for multi-turn continuity (see [Thinking encryption](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-encryption)). This is the default on Claude Fable 5, Claude Mythos 5, Claude Opus 5, Claude Sonnet 5, Claude Opus 4.8, Claude Opus 4.7, and [Claude Mythos Preview](https://anthropic.com/glasswing). Set `display: "omitted"` when your application doesn't surface thinking content to users. The primary benefit is faster time-to-first-text-token when streaming: the server skips streaming thinking tokens entirely and delivers only the signature, so the final text response begins streaming sooner. With `display: "omitted"`, the response contains `thinking` blocks with an empty `thinking` field: ```json Output { "content": [ { "type": "thinking", "thinking": "", "signature": "EosnCkYICxIMMb3LzNrMu..." }, { "type": "text", "text": "The answer is 12,231." } ] } ``` Keep the following in mind when working with omitted thinking: * You're still charged for the full thinking tokens. Omitting reduces latency, not cost. * If you pass thinking blocks back in multi-turn conversations, pass them unchanged. The server decrypts the `signature` to reconstruct the original thinking for prompt construction (see [Preserving thinking blocks](https://platform.claude.com/docs/en/build-with-claude/thinking#preserving-thinking-blocks)). Any text you place in the `thinking` field of a round-tripped omitted block is ignored. * `display` is invalid with `thinking.type: "disabled"` (there is nothing to display). * When using `thinking.type: "adaptive"` and the model skips thinking for a simple request, no thinking block is produced regardless of `display`. * When streaming with `display: "omitted"`, no `thinking_delta` events are emitted. See [Streaming thinking](https://platform.claude.com/docs/en/build-with-claude/thinking#streaming-thinking) for the event sequence. The `signature` field is identical whether `display` is `"summarized"` or `"omitted"`. Switching `display` values between turns in a conversation is supported. In the Ruby SDK, plain hashes take `display:` as the examples show. The typed `ThinkingConfigAdaptive` class names the parameter `display_` (trailing underscore, to avoid shadowing Ruby's `Kernel#display`). Either way, the wire field is still `display`. ### Summarized thinking When `display` is `"summarized"`, the thinking text you receive is a summary of Claude's full thinking process rather than the raw chain of thought. Summarized thinking provides the full intelligence benefits of thinking while preventing misuse. No `display` setting returns the raw chain of thought. Keep the following in mind when working with summarized thinking: * You're charged for the full thinking tokens generated by the original request, not the summary tokens. The billed output token count does not match the count of tokens you see in the response. * On Claude Opus 4.6, Claude Sonnet 4.6, and earlier models, the first few lines of thinking output are more verbose, providing detailed reasoning that's particularly helpful for prompt engineering purposes. [Claude Mythos Preview](https://anthropic.com/glasswing) summarizes from the first token, so its thinking blocks do not show this verbose preamble. * Summarization preserves the key ideas of Claude's thinking process with minimal added latency, so summaries can stream as they arrive. * Summarization is processed by a different model from the one you target in your requests. The thinking model does not see the summarized output. * As Anthropic seeks to improve the thinking feature, summarization behavior is subject to change. In rare cases where you need access to full thinking output, [contact Anthropic sales](mailto:sales@anthropic.com). ### Streaming thinking Thinking works with [streaming](https://platform.claude.com/docs/en/build-with-claude/streaming). Thinking blocks stream as `thinking_delta` events inside `content_block_delta` events, followed by a single `signature_delta` event just before the block's `content_block_stop`. Text blocks stream afterward as usual. ![Diagram of the streaming event sequence with thinking: the thinking block opens, thinking deltas stream only when display is summarized, a single signature delta closes the block, then text deltas stream](https://platform.claude.com/docs/images/how-thinking-streams.svg) The following examples stream a response with adaptive thinking, printing thinking and text deltas as they arrive: ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-4-8", "max_tokens": 16000, "stream": true, "thinking": { "type": "adaptive", "display": "summarized" }, "messages": [ { "role": "user", "content": "What is the greatest common divisor of 1071 and 462?" } ] }' ``` ```bash CLI ant messages create \ --model claude-opus-4-8 \ --max-tokens 16000 \ --thinking '{type: adaptive, display: summarized}' \ --message '{role: user, content: "What is the greatest common divisor of 1071 and 462?"}' \ --stream \ --format jsonl ``` ```python Python client = anthropic.Anthropic() with client.messages.stream( model="claude-opus-4-8", max_tokens=16000, thinking={"type": "adaptive", "display": "summarized"}, messages=[ { "role": "user", "content": "What is the greatest common divisor of 1071 and 462?", } ], ) as stream: for event in stream: if event.type == "content_block_start": print(f"\nStarting {event.content_block.type} block...") elif event.type == "content_block_delta": if event.delta.type == "thinking_delta": print(event.delta.thinking, end="", flush=True) elif event.delta.type == "text_delta": print(event.delta.text, end="", flush=True) ``` ```typescript TypeScript const client = new Anthropic(); const stream = client.messages.stream({ model: "claude-opus-4-8", max_tokens: 16000, thinking: { type: "adaptive", display: "summarized" }, messages: [{ role: "user", content: "What is the greatest common divisor of 1071 and 462?" }] }); for await (const event of stream) { if (event.type === "content_block_start") { console.log(`\nStarting ${event.content_block.type} block...`); } else if (event.type === "content_block_delta") { if (event.delta.type === "thinking_delta") { process.stdout.write(event.delta.thinking); } else if (event.delta.type === "text_delta") { process.stdout.write(event.delta.text); } } } ``` ```csharp C# AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus4_8, MaxTokens = 16000, Thinking = new ThinkingConfigAdaptive { Display = Display.Summarized }, Messages = [new() { Role = Role.User, Content = "What is the greatest common divisor of 1071 and 462?" }] }; await foreach (var rawEvent in client.Messages.CreateStreaming(parameters)) { if (rawEvent.TryPickContentBlockStart(out var start)) { Console.WriteLine($"\nStarting {start.ContentBlock.Type} block..."); } else if (rawEvent.TryPickContentBlockDelta(out var delta)) { if (delta.Delta.TryPickThinking(out var thinkingDelta)) { Console.Write(thinkingDelta.Thinking); } else if (delta.Delta.TryPickText(out var textDelta)) { Console.Write(textDelta.Text); } } } ``` ```go Go client := anthropic.NewClient() stream := client.Messages.NewStreaming(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus4_8, MaxTokens: 16000, Thinking: anthropic.ThinkingConfigParamUnion{ OfAdaptive: &anthropic.ThinkingConfigAdaptiveParam{ Display: anthropic.ThinkingConfigAdaptiveDisplaySummarized, }, }, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("What is the greatest common divisor of 1071 and 462?")), }, }) for stream.Next() { event := stream.Current() switch eventVariant := event.AsAny().(type) { case anthropic.ContentBlockStartEvent: fmt.Printf("\nStarting %s block...\n", eventVariant.ContentBlock.Type) case anthropic.ContentBlockDeltaEvent: switch deltaVariant := eventVariant.Delta.AsAny().(type) { case anthropic.ThinkingDelta: fmt.Print(deltaVariant.Thinking) case anthropic.TextDelta: fmt.Print(deltaVariant.Text) } } } if err := stream.Err(); err != nil { log.Fatal(err) } ``` ```java Java import com.anthropic.models.messages.ThinkingConfigAdaptive; void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_4_8) .maxTokens(16000L) .thinking(ThinkingConfigAdaptive.builder() .display(ThinkingConfigAdaptive.Display.SUMMARIZED) .build()) .addUserMessage("What is the greatest common divisor of 1071 and 462?") .build(); try (var streamResponse = client.messages().createStreaming(params)) { streamResponse.stream().forEach(event -> { if (event.contentBlockStart().isPresent()) { var startEvent = event.contentBlockStart().get(); var block = startEvent.contentBlock(); if (block.isThinking()) { IO.println("\nStarting thinking block..."); } else if (block.isText()) { IO.println("\nStarting text block..."); } } else if (event.contentBlockDelta().isPresent()) { var deltaEvent = event.contentBlockDelta().get(); deltaEvent.delta().thinking().ifPresent(td -> IO.print(td.thinking()) ); deltaEvent.delta().text().ifPresent(td -> IO.print(td.text()) ); } }); } } ``` ```php PHP $client = new Client(); $stream = $client->messages->createStream( maxTokens: 16000, messages: [ ['role' => 'user', 'content' => 'What is the greatest common divisor of 1071 and 462?'] ], model: 'claude-opus-4-8', thinking: ['type' => 'adaptive', 'display' => 'summarized'], ); foreach ($stream as $event) { if ($event->type === 'content_block_start') { echo "\nStarting {$event->contentBlock->type} block...\n"; } elseif ($event->type === 'content_block_delta') { if ($event->delta->type === 'thinking_delta') { echo $event->delta->thinking; } elseif ($event->delta->type === 'text_delta') { echo $event->delta->text; } } } ``` ```ruby Ruby client = Anthropic::Client.new stream = client.messages.stream( model: "claude-opus-4-8", max_tokens: 16000, thinking: { type: "adaptive", display: "summarized" }, messages: [ { role: "user", content: "What is the greatest common divisor of 1071 and 462?" } ] ) stream.each do |event| case event when Anthropic::Streaming::ThinkingEvent print event.thinking when Anthropic::Streaming::TextEvent print event.text end end ``` To reassemble complete thinking blocks with their signatures after streaming, use your SDK's message-accumulation helper where one exists (for example, `stream.get_final_message()` in Python or `stream.finalMessage()` in TypeScript) instead of concatenating deltas yourself. ```sse Output event: message_start data: {"type": "message_start", "message": {"id": "msg_01...", "type": "message", "role": "assistant", "content": [], "model": "claude-opus-4-8", "stop_reason": null, "stop_sequence": null}} event: content_block_start data: {"type": "content_block_start", "index": 0, "content_block": {"type": "thinking", "thinking": "", "signature": ""}} event: content_block_delta data: {"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_delta", "thinking": "I need to find the GCD of 1071 and 462 using the Euclidean algorithm.\n\n1071 = 2 × 462 + 147"}} event: content_block_delta data: {"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_delta", "thinking": "\n462 = 3 × 147 + 21\n147 = 7 × 21 + 0\n\nSo GCD(1071, 462) = 21"}} // Additional thinking deltas... event: content_block_delta data: {"type": "content_block_delta", "index": 0, "delta": {"type": "signature_delta", "signature": "EqQBCgIYAhIM1gbcDa9GJwZA2b..."}} event: content_block_stop data: {"type": "content_block_stop", "index": 0} event: content_block_start data: {"type": "content_block_start", "index": 1, "content_block": {"type": "text", "text": ""}} event: content_block_delta data: {"type": "content_block_delta", "index": 1, "delta": {"type": "text_delta", "text": "The greatest common divisor of 1071 and 462 is **21**."}} // Additional text deltas... event: content_block_stop data: {"type": "content_block_stop", "index": 1} event: message_delta data: {"type": "message_delta", "delta": {"stop_reason": "end_turn", "stop_sequence": null}} event: message_stop data: {"type": "message_stop"} ``` When `display: "omitted"` is set, the thinking block opens, a single `signature_delta` arrives, and the block closes without any `thinking_delta` events. Text streaming begins immediately after: ```sse Output event: content_block_start data: {"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":"","signature":""}} event: content_block_delta data: {"type":"content_block_delta","index":0,"delta":{"type":"signature_delta","signature":"EosnCkYICxIMMb3LzNrMu..."}} event: content_block_stop data: {"type":"content_block_stop","index":0} event: content_block_start data: {"type":"content_block_start","index":1,"content_block":{"type":"text","text":""}} ``` When using streaming with thinking enabled, you might notice that text sometimes arrives in larger chunks alternating with smaller, token-by-token delivery. This is expected behavior, especially for thinking content. The streaming system processes content in batches, which can delay and group streaming events into this "chunky" delivery pattern. For general streaming mechanics, see [Streaming Messages](https://platform.claude.com/docs/en/build-with-claude/streaming). ## Thinking and effort The `thinking` parameter controls whether Claude thinks in [thinking blocks](https://platform.claude.com/docs/en/build-with-claude/thinking) before answering; the `effort` parameter controls how much work Claude puts into the whole response, which in adaptive mode includes how often and how deeply it thinks. Don't pass `adaptive` as an `effort` value: `adaptive` is a thinking mode, not an effort level. For what each effort level does to thinking behavior, see the [per-level thinking behavior table](https://platform.claude.com/docs/en/build-with-claude/thinking-steering-and-cost#effort-levels) on the [Steering thinking](https://platform.claude.com/docs/en/build-with-claude/thinking-steering-and-cost) page. The [Effort](https://platform.claude.com/docs/en/build-with-claude/effort) page documents the parameter itself, including which levels each model supports. On Claude Opus 4.5, the only extended-thinking-only model that supports effort, effort composes with `budget_tokens`. See [Budget rules and tuning](https://platform.claude.com/docs/en/build-with-claude/extended-thinking#budget-rules-and-tuning). With the two controls separated this way, pick the one that matches your goal: * **Lower cost or latency on a thinking-enabled workload:** lower `effort` first. It scales the whole response down, thinking included. * **Claude is thinking too rarely or too shallowly:** raise `effort`, or see [Steering how often Claude thinks](https://platform.claude.com/docs/en/build-with-claude/thinking-steering-and-cost#tuning-thinking-behavior) on the steering page. * **You need thinking fully off:** use `thinking: {type: "disabled"}` on models that allow it (see the [per-model configuration table](https://platform.claude.com/docs/en/build-with-claude/thinking-troubleshooting#supported-models)). * **You need a hard ceiling on spend:** use `max_tokens`. Effort is soft guidance. `max_tokens` is a strict limit. ## Thinking with tool use Thinking works alongside [tool use](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview), letting Claude reason through tool selection and process tool results. Two constraints apply: 1. **Tool choice limitation (manual mode):** tool use with manual extended thinking (`thinking: {type: "enabled"}`) only supports `tool_choice: {"type": "auto"}` (the default) or `tool_choice: {"type": "none"}`. Using `tool_choice: {"type": "any"}` or `tool_choice: {"type": "tool", "name": "..."}` results in an error because these options force tool use, which is incompatible with manual extended thinking. Adaptive thinking, including on models where thinking is on by default, supports forced tool use. 2. **Preserving thinking blocks:** when you return tool results, you must pass the thinking blocks from the assistant message back to the API, complete and unmodified. See [Preserving thinking blocks](https://platform.claude.com/docs/en/build-with-claude/thinking#preserving-thinking-blocks). **A tool-use loop is one assistant turn.** From the model's perspective, an assistant turn doesn't complete until Claude finishes its full response, which may include multiple tool calls and results. This whole sequence is a single assistant turn: ```text wrap User: "What's the weather in Paris?" Assistant: [thinking] + [tool_use: get_weather] User: [tool_result: "20°C, sunny"] Assistant: [text: "The weather in Paris is 20°C and sunny"] ``` The entire turn runs in a single thinking mode: you can't toggle thinking in the middle of a turn, including during the tool-use loop. In extended (manual) mode, the API additionally enforces that the final assistant turn of a thinking-enabled request begins with a thinking block. Adaptive mode relaxes this: no assistant turn needs to start with one. **Mid-turn conflicts degrade gracefully.** If you toggle thinking mid-turn (for example, between sending a tool call and returning its result), the API doesn't error. Instead, it silently disables thinking for that request. To preserve model quality, the API may strip thinking blocks that would create an invalid turn structure, or disable thinking when the conversation history is incompatible with thinking being enabled. To confirm whether thinking was active, check for the presence of `thinking` blocks in the response. **Toggle between turns, not within them.** Plan your thinking strategy at the start of each turn. Complete the assistant turn, then change the thinking configuration for the next one: ```text wrap User: "What's the weather?" Assistant: [tool_use] (thinking disabled) User: [tool_result] Assistant: [text: "It's sunny"] User: "What about tomorrow?" Assistant: [thinking] + [text: "..."] (thinking enabled - new turn) ``` Toggling thinking modes also invalidates prompt caching. See [Thinking and prompt caching](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-and-prompt-caching). ### Preserving thinking blocks When Claude invokes a tool, it pauses construction of its response to await external information. When you return the tool result, Claude continues building that same response, so its earlier reasoning must still be present. Pass every `thinking` block back to the API complete and unmodified, alongside the `tool_use` block it accompanied. This matters for two reasons: 1. **Reasoning continuity:** the thinking blocks capture the step-by-step reasoning that led to the tool requests. Including them lets Claude continue reasoning from where it left off. 2. **Context maintenance:** tool results appear as user messages in the API structure, but they're part of one continuous reasoning flow. Preserving thinking blocks maintains that flow across API calls. In short: * **Required:** within a tool-use turn, pass thinking blocks back. * **Recommended:** across turns, pass everything back. * **Allowed:** outside tool use, omit prior turns' thinking. You don't need to prune old thinking yourself. Pass all thinking blocks back in multi-turn conversations, and the API automatically filters them, keeps the blocks needed to preserve the model's reasoning, and bills input tokens only for the blocks actually shown to Claude. Which prior-turn blocks are kept is per-model. See [Thinking block preservation by model](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-block-preservation-by-model). To override the default, use the [`clear_thinking_20251015` context-editing strategy](https://platform.claude.com/docs/en/build-with-claude/context-editing#thinking-block-clearing). Within the latest assistant message, the sequence of consecutive `thinking` blocks must match what the model generated in the original request: you can't rearrange, edit, or partially drop them. This includes [`redacted_thinking` blocks](https://platform.claude.com/docs/en/build-with-claude/thinking#redacted-thinking-blocks). Modified thinking blocks are rejected with a 400 error. See [A 400 error says thinking blocks cannot be modified](https://platform.claude.com/docs/en/build-with-claude/thinking-troubleshooting#error-thinking-blocks-modified) for the exact message, the common causes, and the fix. The one exception: text placed in the empty `thinking` field of an [omitted](https://platform.claude.com/docs/en/build-with-claude/thinking#controlling-thinking-display) block is ignored rather than rejected. For a complete two-turn walkthrough with code in every SDK, see [Thinking in tool and multi-turn workflows](https://platform.claude.com/docs/en/build-with-claude/thinking-tool-workflows#two-turn-tool-use-round-trip). It defines a tool, receives a thinking-plus-tool-use response, and echoes the assistant turn back with the tool result. ### Interleaved thinking Interleaved thinking lets Claude think between tool calls, reasoning about each tool result before acting on it. With interleaved thinking, Claude can: * Reason about the results of a tool call before deciding what to do next * Chain multiple tool calls with reasoning steps in between * Make more nuanced decisions based on intermediate results Consecutive tool calls do not require interleaved thinking. Claude can chain tool calls with or without interleaved thinking. Interleaving changes where thinking blocks appear between tool calls, not whether tool calls can chain. With adaptive thinking, interleaved thinking is automatic on every model that supports adaptive thinking. No beta header is needed. On Claude Fable 5, Claude Mythos 5, Claude Mythos Preview, Claude Opus 5, Claude Opus 4.8, and Claude Opus 4.7, reasoning between tool calls always appears in thinking blocks. Claude Haiku 4.5 does not support interleaved thinking. On models using manual extended thinking, interleaving requires a beta header and changes how the thinking budget is counted. [Interleaved thinking in manual mode](https://platform.claude.com/docs/en/build-with-claude/extended-thinking#interleaved-thinking) covers the per-model rules and platform-specific header behavior. With interleaved thinking, the thinking allocation can span the entire assistant turn rather than a single response. Interleaved thinking is only supported for [tools used through the Messages API](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview). For a worked comparison showing what interleaved thinking changes in a two-tool workflow, see [How interleaved thinking changes the flow](https://platform.claude.com/docs/en/build-with-claude/thinking-tool-workflows#how-interleaved-thinking-changes-the-flow). ### Thinking block preservation by model Whether thinking blocks from previous assistant turns stay in context by default depends on the model: * **Keep all prior turns:** Claude Opus 4.5 and later Opus models, Claude Sonnet 4.6 and later Sonnet models, Claude Fable 5, Claude Mythos 5, and Claude Mythos Preview. * **Keep the last turn only:** earlier Opus and Sonnet models, and all Haiku models through Claude Haiku 4.5. When you pass older thinking blocks back, the API strips them automatically. You don't need to remove them yourself. Preservation brings two benefits: * **Cache optimization:** preserved thinking blocks enable cache hits during tool use, as they are passed back with tool results and cached incrementally across the assistant turn, resulting in token savings in multistep workflows. * **No intelligence impact:** preserving thinking blocks has no negative effect on model performance. The tradeoff is context usage: long conversations consume more context space on keep-all models, because retained thinking blocks count as input like any other conversation history (see [Thinking and the context window](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-and-the-context-window)). The behavior is automatic in both regimes. No code changes or beta headers are required, and you should keep passing complete, unmodified thinking blocks back as described in [Preserving thinking blocks](https://platform.claude.com/docs/en/build-with-claude/thinking#preserving-thinking-blocks). To override the default in either direction, use [thinking block clearing](https://platform.claude.com/docs/en/build-with-claude/context-editing#thinking-block-clearing). **Switching models mid-conversation.** When you switch between any two models, for example after a [classifier refusal fallback](https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback), strip `thinking` and `redacted_thinking` blocks from prior assistant turns. Thinking blocks are tied to the model that produced them. Other models silently ignore them rather than rejecting the request, but ignored blocks still add input tokens. ## Thinking and prompt caching [Prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) interacts with thinking in a few specific ways. The following rules apply in both thinking modes. **Configuration changes invalidate caching.** The thinking configuration and the resolved [`effort`](https://platform.claude.com/docs/en/build-with-claude/effort) level are rendered into the prompt itself, so changing any of them starts a new cache prefix. Switching between `adaptive`, `enabled`, and `disabled`, changing `budget_tokens`, and changing the effort value all invalidate cache breakpoints: message-level breakpoints always miss, and tool and system-prompt breakpoints can miss too, depending on where the model renders the configuration. Treat any thinking or effort change as starting the cache over. Consecutive requests that keep the same configuration preserve the cache, and setting a parameter explicitly to its default value is equivalent to omitting it. A worked demonstration with usage output is on the [Steering thinking](https://platform.claude.com/docs/en/build-with-claude/thinking-steering-and-cost#prompt-caching) page. **Thinking blocks are cached with tool results.** During a tool-use loop, caching occurs when you make a follow-up request that includes tool results. At that point the previous conversation history, including its thinking blocks, can be cached, and those cached thinking blocks count as input tokens in your usage metrics when read from the cache. This occurs automatically, even without explicit `cache_control` markers, and behaves the same for regular and interleaved thinking. The tradeoff: thinking blocks you never see again in responses still contribute to input token usage when read from cache. **Whether prior blocks are in context at all is per-model.** The [preservation default](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-block-preservation-by-model) governs this. On keep-all models, previous turns' thinking blocks stay cached and in context. On last-turn-only models, once you send a user message that isn't a tool result, all previous thinking blocks are stripped from context. On those models, a conversation like this: ```text wrap User: ["What's the weather in Paris?"], Assistant: [thinking_block_1] + [tool_use block 1], User: [tool_result_1, cache=True], Assistant: [thinking_block_2] + [text block 2], User: [Text response, cache=True] ``` is processed as if the thinking blocks were never there: ```text wrap User: ["What's the weather in Paris?"], Assistant: [tool_use block 1], User: [tool_result_1, cache=True], Assistant: [text block 2], User: [Text response, cache=True] ``` On keep-all models, the same request keeps `thinking_block_1` and `thinking_block_2` in context and in the cache. **Degradation strips thinking from the cacheable history.** If thinking becomes disabled mid-turn and you pass thinking content in the current tool-use turn, the thinking content is stripped and thinking remains disabled for that request (see [graceful degradation](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-with-tool-use)). [Interleaved thinking](https://platform.claude.com/docs/en/build-with-claude/thinking#interleaved-thinking) amplifies cache invalidation effects, because thinking blocks can occur between multiple tool calls. Thinking-heavy tasks often take longer than the default 5-minute cache lifetime to complete. Consider the [1-hour cache duration](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#1-hour-cache-duration) to maintain cache hits across longer thinking sessions and multistep workflows. ## Thinking and the context window `max_tokens`, which includes all thinking Claude generates in the current turn, is enforced as a strict limit. On Claude 4.5 models and newer, if input tokens plus `max_tokens` exceeds the context window size, the API accepts the request. If generation then reaches the context window limit, it stops with `stop_reason: "model_context_window_exceeded"` instead of returning an error. On earlier models, the API returns a validation error instead. See [Handling stop reasons](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons). How thinking counts against the window depends on when it was generated: * **Current-turn thinking** always counts toward `max_tokens`, is billed as output tokens, and occupies context window space for the turn that generated it. * **Prior-turn thinking** depends on the [preservation default](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-block-preservation-by-model). On [models that keep all prior turns](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-block-preservation-by-model), previous thinking blocks remain in context, count toward the window, and are billed as input tokens like the rest of the conversation history. On models that keep only the last turn, the API strips older thinking blocks automatically when you pass them back, so they don't consume window space or input tokens. In practice: * On keep-all models, budget your context window as if thinking were ordinary conversation history, because it is. Long agentic sessions accumulate thinking in context. Use [thinking block clearing](https://platform.claude.com/docs/en/build-with-claude/context-editing#thinking-block-clearing) if you need to reclaim space. * On last-turn-only models, thinking is a per-turn cost only: each turn's thinking counts against that turn's `max_tokens` and then drops out of the window. The following diagrams illustrate the last-turn-only (stripping) regime. The first shows a multi-turn conversation: each turn's thinking block is generated in the output but not carried into later turns' input. ![Diagram of thinking on a model that strips previous thinking blocks: each turn's thinking block is generated in the output and not carried into later turns' input](https://platform.claude.com/docs/images/context-window-thinking.svg) The second shows the same regime with tool use: thinking stays in context alongside its tool result for the duration of the assistant turn, then drops out on the next user turn. ![Diagram of thinking with tool use on a model that strips previous thinking blocks: thinking is kept with its tool result, then dropped on the next user turn](https://platform.claude.com/docs/images/context-window-thinking-tools.svg) Use the [token counting API](https://platform.claude.com/docs/en/build-with-claude/token-counting) to get accurate counts for your specific use case, especially for multi-turn conversations that include thinking. ## Thinking encryption Full thinking content is encrypted and returned in the `signature` field on each thinking block. The API uses the signature to verify that thinking blocks were generated by Claude when you pass them back. Keep the following in mind when working with signatures: * It is only strictly necessary to send back thinking blocks when [using tools with thinking](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-with-tool-use). Otherwise you can omit thinking blocks from previous turns. If you do pass them back, whether the API keeps or strips them depends on the model (see [Thinking block preservation by model](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-block-preservation-by-model)). Use [context editing](https://platform.claude.com/docs/en/build-with-claude/context-editing) to configure this. * When sending back thinking blocks, pass everything back exactly as you received it, for consistency and to avoid potential issues. * When [streaming responses](https://platform.claude.com/docs/en/build-with-claude/thinking#streaming-thinking), the signature arrives as a `signature_delta` inside a `content_block_delta` event just before the `content_block_stop` event. * `signature` values are significantly longer in Claude 4 and later models than in previous models. * The `signature` field is opaque: don't interpret or parse it. * `signature` values are compatible across platforms (the Claude API, [Amazon Bedrock](https://platform.claude.com/docs/en/build-with-claude/claude-in-amazon-bedrock), and [Google Cloud](https://platform.claude.com/docs/en/build-with-claude/claude-on-vertex-ai)). Values generated on one platform work on another. ## Redacted thinking blocks In addition to regular `thinking` blocks, the API may return `redacted_thinking` blocks when portions of Claude's reasoning are safety-redacted. A `redacted_thinking` block contains encrypted thinking content in a `data` field, with no readable text: ```json { "type": "redacted_thinking", "data": "..." } ``` The `data` field is opaque and encrypted. Like the `signature` field on regular thinking blocks, pass `redacted_thinking` blocks back to the API unchanged when continuing a multi-turn conversation with [tools](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-with-tool-use). If your code filters content blocks by type (for example, `block.type == "thinking"`) when round-tripping responses with tool use, also include `redacted_thinking` blocks. Filtering on `block.type == "thinking"` alone silently drops `redacted_thinking` blocks and breaks the multi-turn protocol described in [Preserving thinking blocks](https://platform.claude.com/docs/en/build-with-claude/thinking#preserving-thinking-blocks). `redacted_thinking` blocks are a distinct content block type returned when thinking is safety-redacted. This is separate from the [`display: "omitted"`](https://platform.claude.com/docs/en/build-with-claude/thinking#controlling-thinking-display) option, which returns regular `thinking` blocks with an empty `thinking` field. ## Thinking output on Claude Fable 5 and Claude Mythos 5 On Claude Fable 5 and Claude Mythos 5, the raw chain of thought is never returned. The blocks you receive are regular `thinking` blocks, not `redacted_thinking`, and the [`display` setting](https://platform.claude.com/docs/en/build-with-claude/thinking#controlling-thinking-display) works the same as on other models ([summarized](https://platform.claude.com/docs/en/build-with-claude/thinking#summarized-thinking) text, or an empty `thinking` field when omitted, the default here). For the response shape of thinking blocks, see the [Messages API reference](https://platform.claude.com/docs/en/api/messages/create). When continuing a conversation on the same model, pass each thinking block back to the API exactly as received, including blocks whose `thinking` field is empty. Don't edit or reconstruct them. Reading the summary text for display is fine: the API rejects blocks whose returned content has been modified, not blocks you have read. Text placed in an empty omitted `thinking` field is [ignored rather than rejected](https://platform.claude.com/docs/en/build-with-claude/thinking#controlling-thinking-display). For how thinking blocks are handled when you switch models mid-conversation, see [Thinking block preservation by model](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-block-preservation-by-model). Two exceptions, covered in [Fallback credit](https://platform.claude.com/docs/en/build-with-claude/fallback-credit): * Fallback-credit retries must echo the refused request body unchanged. * `fallback` blocks from a mid-output fallback stay where they appeared. To get visibility into the model's reasoning, read the `thinking` blocks described on this page rather than prompting for reasoning in the response text. On Claude Fable 5, a request that attempts to elicit the model's internal reasoning as part of the response text can be refused with `stop_details.category: "reasoning_extraction"`. See [Refusal categories](https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback#refusal-response) for the field reference and handling guidance. ## Limits and feature compatibility **Sampling parameters.** On Claude Fable 5, Claude Mythos 5, Claude Mythos Preview, Claude Opus 5, Claude Opus 4.8, Claude Opus 4.7, and Claude Sonnet 5, non-default `temperature`, `top_p`, or `top_k` values return a 400 error on every request, regardless of whether thinking is used. On older models, the restriction applies only while thinking is on: `temperature` and `top_k` are incompatible with thinking, and `top_p` is allowed at values between 0.95 and 1. **Response prefill and forced tool use.** You can't pre-fill the assistant response while thinking is on. Forced tool use (`tool_choice: {"type": "any"}` or `{"type": "tool", ...}`) is incompatible with manual extended thinking but works with adaptive thinking. See [Thinking with tool use](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-with-tool-use). **Output limits.** Claude Fable 5, Claude Mythos 5, Claude Mythos Preview, Claude Opus 5, Claude Opus 4.8, Claude Opus 4.7, Claude Sonnet 5, Claude Opus 4.6, and Claude Sonnet 4.6 support up to 128k output tokens per request. Claude Haiku 4.5, Claude Sonnet 4.5, and Claude Opus 4.5 support up to 64k. On the [Message Batches API](https://platform.claude.com/docs/en/build-with-claude/batch-processing#extended-output-beta), the `output-300k-2026-03-24` [beta header](https://platform.claude.com/docs/en/api/beta-headers) raises the limit to 300k for Claude Opus 5, Claude Opus 4.8, Claude Opus 4.7, Claude Sonnet 5, Claude Opus 4.6, and Claude Sonnet 4.6. See the [models overview](https://platform.claude.com/docs/en/about-claude/models/overview) for limits on legacy models. **Long requests.** The SDKs require streaming when `max_tokens` is greater than 21,333, to avoid HTTP timeouts on long-running requests. This is a client-side validation, not an API restriction. If you don't need to process events incrementally, use `.stream()` with `.get_final_message()` (Python) or `.finalMessage()` (TypeScript) to get the complete `Message` object without handling individual events. See [Streaming Messages](https://platform.claude.com/docs/en/build-with-claude/streaming#get-the-final-message-without-handling-events). Expect longer response times when thinking is active, because generating thinking blocks adds processing time. For workloads that push thinking above roughly 32k tokens per request, use [batch processing](https://platform.claude.com/docs/en/build-with-claude/batch-processing) to avoid networking issues: such requests can run long enough to hit system timeouts and open connection limits. ## Next steps Steer how often and how deeply Claude thinks with effort levels, system prompt guidance, and per-message steering, and understand thinking's cost and pricing. Walk through a complete two-turn tool-use round trip that preserves thinking blocks correctly, and see how interleaved thinking changes the flow. Diagnose and fix the most common thinking failures: configuration 400 errors, empty or missing thinking blocks, max\_tokens stops, and cache misses. Control how many tokens Claude uses when responding with the effort parameter, trading off between response thoroughness and token efficiency. --- title: Thinking in tool and multi-turn workflows url: https://platform.claude.com/docs/en/build-with-claude/thinking-tool-workflows description: Walk through a complete two-turn tool-use round trip that preserves thinking blocks correctly, and see how interleaved thinking changes the flow. --- For how zero data retention (ZDR) applies to this feature, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention). This page walks through a complete two-turn tool-use round trip with thinking enabled: Claude thinks, requests a tool call, receives the result, and finishes its answer, with the thinking blocks handled correctly at every step. The full rules live on the [Thinking](https://platform.claude.com/docs/en/build-with-claude/thinking) page, in [Thinking with tool use](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-with-tool-use) and [Preserving thinking blocks](https://platform.claude.com/docs/en/build-with-claude/thinking#preserving-thinking-blocks); this page shows those rules applied in runnable code. ## The rules this walkthrough applies Each link leads to the full statement on the Thinking page: * [Limit tool choice to `auto` or `none` in manual mode](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-with-tool-use): `tool_choice` options that force tool use return an error with manual extended thinking (`thinking: {type: "enabled"}`); adaptive thinking supports forced tool use. * [Keep one thinking configuration per assistant turn](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-with-tool-use): a tool-use loop is one assistant turn, so change the configuration only between turns. * [Pass thinking blocks back complete and unmodified](https://platform.claude.com/docs/en/build-with-claude/thinking#preserving-thinking-blocks): when you return a tool result, the thinking blocks from the assistant message must come back with it. * [Echo the assistant message exactly as received](https://platform.claude.com/docs/en/build-with-claude/thinking#preserving-thinking-blocks): rebuilding the message or filtering out `redacted_thinking` blocks triggers a 400 error. The samples use adaptive thinking; on models that support only extended thinking, substitute `thinking: {type: "enabled", budget_tokens: N}`. The round-trip rules are identical. ## Walk through a two-turn tool-use round trip The example defines a `get_weather` tool, lets Claude think and request a tool call, then returns the tool result along with the assistant turn echoed exactly as received, thinking block included. Send a request with adaptive thinking enabled and the tool defined. Apart from the `thinking` parameter, this is a standard [tool use](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview) request: ```bash CLI ant messages create --transform content <<'YAML' model: claude-opus-4-8 max_tokens: 16000 thinking: type: adaptive tools: - name: get_weather description: Get current weather for a location input_schema: type: object properties: location: type: string description: City name required: - location messages: - role: user content: "What's the weather in Paris?" YAML ``` ```python Python client = anthropic.Anthropic() weather_tool = { "name": "get_weather", "description": "Get current weather for a location", "input_schema": { "type": "object", "properties": {"location": {"type": "string", "description": "City name"}}, "required": ["location"], }, } # First request - Claude responds with thinking and tool request response = client.messages.create( model="claude-opus-4-8", max_tokens=16000, thinking={"type": "adaptive"}, tools=[weather_tool], messages=[{"role": "user", "content": "What's the weather in Paris?"}], ) print(response) ``` ```typescript TypeScript const client = new Anthropic(); const weatherTool: Anthropic.Tool = { name: "get_weather", description: "Get current weather for a location", input_schema: { type: "object", properties: { location: { type: "string", description: "City name" } }, required: ["location"] } }; // First request - Claude responds with thinking and tool request const response = await client.messages.create({ model: "claude-opus-4-8", max_tokens: 16000, thinking: { type: "adaptive" }, tools: [weatherTool], messages: [{ role: "user", content: "What's the weather in Paris?" }] }); console.log(response); ``` ```csharp C# AnthropicClient client = new(); var weatherTool = new ToolUnion(new Tool() { Name = "get_weather", Description = "Get current weather for a location", InputSchema = new InputSchema() { Properties = new Dictionary { ["location"] = JsonSerializer.SerializeToElement(new { type = "string", description = "City name" }), }, Required = ["location"], }, }); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus4_8, MaxTokens = 16000, Thinking = new ThinkingConfigAdaptive(), Tools = [weatherTool], Messages = [new() { Role = Role.User, Content = "What's the weather in Paris?" }] }; var message = await client.Messages.Create(parameters); Console.WriteLine(message); ``` ```go Go client := anthropic.NewClient() weatherTool := anthropic.ToolUnionParam{ OfTool: &anthropic.ToolParam{ Name: "get_weather", Description: anthropic.String("Get current weather for a location"), InputSchema: anthropic.ToolInputSchemaParam{ Properties: map[string]any{ "location": map[string]any{ "type": "string", "description": "City name", }, }, Required: []string{"location"}, }, }, } response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus4_8, MaxTokens: 16000, Thinking: anthropic.ThinkingConfigParamUnion{ OfAdaptive: &anthropic.ThinkingConfigAdaptiveParam{}, }, Tools: []anthropic.ToolUnionParam{weatherTool}, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("What's the weather in Paris?")), }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java import com.anthropic.models.messages.ThinkingConfigAdaptive; // ... AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_4_8) .maxTokens(16000L) .thinking(ThinkingConfigAdaptive.builder().build()) .addTool(Tool.builder() .name("get_weather") .description("Get current weather for a location") .inputSchema(Tool.InputSchema.builder() .properties(JsonValue.from(Map.of( "location", Map.of("type", "string", "description", "City name") ))) .required(List.of("location")) .build()) .build()) .addUserMessage("What's the weather in Paris?") .build(); Message response = client.messages().create(params); IO.println(response); ``` ```php PHP $client = new Client(); $weatherTool = [ 'name' => 'get_weather', 'description' => 'Get current weather for a location', 'input_schema' => [ 'type' => 'object', 'properties' => [ 'location' => ['type' => 'string', 'description' => 'City name'] ], 'required' => ['location'] ] ]; $message = $client->messages->create( maxTokens: 16000, messages: [ ['role' => 'user', 'content' => "What's the weather in Paris?"] ], model: 'claude-opus-4-8', thinking: ['type' => 'adaptive'], tools: [$weatherTool], ); echo $message; ``` ```ruby Ruby client = Anthropic::Client.new weather_tool = { name: "get_weather", description: "Get current weather for a location", input_schema: { type: "object", properties: { location: { type: "string", description: "City name" } }, required: ["location"] } } message = client.messages.create( model: "claude-opus-4-8", max_tokens: 16000, thinking: { type: "adaptive" }, tools: [weather_tool], messages: [ { role: "user", content: "What's the weather in Paris?" } ] ) puts message ``` You should see `thinking`, `text`, and `tool_use` blocks in the response content on a run where Claude chose to think (on simpler requests, adaptive mode may skip the thinking block). Keep this content array intact: the next step sends it back verbatim. To see thinking text like this output, add `display: "summarized"` to the request. On models where display defaults to omitted, including claude-opus-4-8, the `thinking` field otherwise comes back as an empty string with only the `signature` populated. Either way, echo the content array back unchanged; see [Controlling thinking display](https://platform.claude.com/docs/en/build-with-claude/thinking#controlling-thinking-display). ```json Output { "content": [ { "type": "thinking", "thinking": "The user wants to know the current weather in Paris. I have access to a function `get_weather`...", "signature": "BDaL4VrbR2Oj0hO4XpJxT28J5T...." }, { "type": "text", "text": "I can help you get the current weather information for Paris. Let me check that for you" }, { "type": "tool_use", "id": "toolu_01CswdEQBMshySk6Y9DFKrfq", "name": "get_weather", "input": { "location": "Paris" } } ] } ``` Run the tool on your side, then send a second request that appends two messages to the conversation. The first is the assistant content echoed back exactly as received, so the thinking block stays unchanged alongside the `tool_use` block. The second is a user message carrying the `tool_result`. Each sample is a self-contained script: it repeats the first request, then immediately sends the follow-up using the response it just received. ```bash CLI # First turn: write the assistant content array (thinking and tool_use # blocks, signatures intact) to a file. Routing model-generated text # through a file keeps it out of shell-expansion position later. ant messages create --transform content --format jsonl \ > assistant_content.json <<'YAML' model: claude-opus-4-8 max_tokens: 16000 thinking: type: adaptive tools: - name: get_weather description: Get current weather for a location input_schema: type: object properties: location: type: string description: City name required: [location] messages: - role: user content: What's the weather in Paris? YAML # Second turn: jq fills the two null placeholders from the captured file, # so the blocks return verbatim as the assistant message. The thinking # block MUST accompany the tool_use block. The quoted delimiter keeps the # shell from expanding anything in the body. jq --slurpfile blocks assistant_content.json ' .messages[1].content = $blocks[0] | .messages[2].content[0].tool_use_id = ($blocks[0][] | select(.type == "tool_use") | .id) ' <<'JSON' | ant messages create { "model": "claude-opus-4-8", "max_tokens": 16000, "thinking": {"type": "adaptive"}, "tools": [{ "name": "get_weather", "description": "Get current weather for a location", "input_schema": { "type": "object", "properties": { "location": {"type": "string", "description": "City name"} }, "required": ["location"] } }], "messages": [ {"role": "user", "content": "What's the weather in Paris?"}, {"role": "assistant", "content": null}, {"role": "user", "content": [{ "type": "tool_result", "tool_use_id": null, "content": "Current temperature: 88°F" }]} ] } JSON ``` ```python Python client = anthropic.Anthropic() weather_tool = { "name": "get_weather", "description": "Get current weather for a location", "input_schema": { "type": "object", "properties": {"location": {"type": "string", "description": "City name"}}, "required": ["location"], }, } response = client.messages.create( model="claude-opus-4-8", max_tokens=16000, thinking={"type": "adaptive"}, tools=[weather_tool], messages=[{"role": "user", "content": "What's the weather in Paris?"}], ) # Extract the tool use block to get its ID for the tool result tool_use_block = next(block for block in response.content if block.type == "tool_use") # Call your actual weather API, here is where your actual API call would go # Let's pretend this is what we get back weather_data = {"temperature": 88} # Second request - Include the assistant turn and the tool result continuation = client.messages.create( model="claude-opus-4-8", max_tokens=16000, thinking={"type": "adaptive"}, tools=[weather_tool], messages=[ {"role": "user", "content": "What's the weather in Paris?"}, # Echo the assistant content exactly as received. When a thinking # block is present, it must accompany the tool_use block. {"role": "assistant", "content": response.content}, { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": tool_use_block.id, "content": f"Current temperature: {weather_data['temperature']}°F", } ], }, ], ) print(continuation) ``` ```typescript TypeScript const client = new Anthropic(); const weatherTool: Anthropic.Tool = { name: "get_weather", description: "Get current weather for a location", input_schema: { type: "object", properties: { location: { type: "string", description: "City name" } }, required: ["location"] } }; const response = await client.messages.create({ model: "claude-opus-4-8", max_tokens: 16000, thinking: { type: "adaptive" }, tools: [weatherTool], messages: [{ role: "user", content: "What's the weather in Paris?" }] }); // Extract the tool use block to get its ID for the tool result const toolUseBlock = response.content.find( (block): block is Anthropic.ToolUseBlock => block.type === "tool_use" ); // Call your actual weather API, here is where your actual API call would go // Let's pretend this is what we get back const weatherData = { temperature: 88 }; if (toolUseBlock) { // Second request - Include the assistant turn and the tool result const continuation = await client.messages.create({ model: "claude-opus-4-8", max_tokens: 16000, thinking: { type: "adaptive" }, tools: [weatherTool], messages: [ { role: "user", content: "What's the weather in Paris?" }, // Echo the assistant content exactly as received. When a thinking // block is present, it must accompany the tool_use block. { role: "assistant", content: response.content }, { role: "user", content: [ { type: "tool_result" as const, tool_use_id: toolUseBlock.id, content: `Current temperature: ${weatherData.temperature}°F` } ] } ] }); console.log(continuation); } ``` ```csharp C# AnthropicClient client = new(); var weatherTool = new ToolUnion(new Tool() { Name = "get_weather", Description = "Get current weather for a location", InputSchema = new InputSchema() { Properties = new Dictionary { ["location"] = JsonSerializer.SerializeToElement(new { type = "string", description = "City name" }), }, Required = ["location"], }, }); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus4_8, MaxTokens = 16000, Thinking = new ThinkingConfigAdaptive(), Tools = [weatherTool], Messages = [ new() { Role = Role.User, Content = "What's the weather in Paris?" } ] }; var response = await client.Messages.Create(parameters); // Extract the tool_use block to get its ID for the tool result ToolUseBlock? toolUseBlock = null; foreach (var block in response.Content) { if (block.TryPickToolUse(out var toolUse)) { toolUseBlock = toolUse; break; } } var weatherData = new { temperature = 88 }; // Build continuation with tool result var continuationParams = new MessageCreateParams { Model = Model.ClaudeOpus4_8, MaxTokens = 16000, Thinking = new ThinkingConfigAdaptive(), Tools = [weatherTool], Messages = [ new() { Role = Role.User, Content = "What's the weather in Paris?" }, // response.Content includes the thinking blocks; passing them back is required new() { Role = Role.Assistant, Content = response.Content.Select(block => new ContentBlockParam(block.Json)).ToList() }, new() { Role = Role.User, Content = new MessageParamContent(new List { new ContentBlockParam(new ToolResultBlockParam() { ToolUseID = toolUseBlock?.ID ?? "", Content = $"Current temperature: {weatherData.temperature}°F" }) })} ] }; var continuation = await client.Messages.Create(continuationParams); Console.WriteLine(continuation); ``` ```go Go client := anthropic.NewClient() weatherTool := anthropic.ToolUnionParam{ OfTool: &anthropic.ToolParam{ Name: "get_weather", Description: anthropic.String("Get current weather for a location"), InputSchema: anthropic.ToolInputSchemaParam{ Properties: map[string]any{ "location": map[string]any{ "type": "string", "description": "City name", }, }, Required: []string{"location"}, }, }, } response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus4_8, MaxTokens: 16000, Thinking: anthropic.ThinkingConfigParamUnion{ OfAdaptive: &anthropic.ThinkingConfigAdaptiveParam{}, }, Tools: []anthropic.ToolUnionParam{weatherTool}, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("What's the weather in Paris?")), }, }) if err != nil { log.Fatal(err) } var toolUseBlock anthropic.ToolUseBlock for _, block := range response.Content { if v, ok := block.AsAny().(anthropic.ToolUseBlock); ok { toolUseBlock = v break } } weatherData := map[string]int{"temperature": 88} continuation, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus4_8, MaxTokens: 16000, Thinking: anthropic.ThinkingConfigParamUnion{ OfAdaptive: &anthropic.ThinkingConfigAdaptiveParam{}, }, Tools: []anthropic.ToolUnionParam{weatherTool}, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("What's the weather in Paris?")), response.ToParam(), anthropic.NewUserMessage( anthropic.NewToolResultBlock(toolUseBlock.ID, fmt.Sprintf("Current temperature: %d°F", weatherData["temperature"]), false), ), }, }) if err != nil { log.Fatal(err) } fmt.Println(continuation) ``` ```java Java import com.anthropic.models.messages.ThinkingConfigAdaptive; // ... void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); Tool weatherTool = Tool.builder() .name("get_weather") .description("Get current weather for a location") .inputSchema(Tool.InputSchema.builder() .properties(JsonValue.from(Map.of( "location", Map.of("type", "string", "description", "City name") ))) .required(List.of("location")) .build()) .build(); MessageCreateParams initialParams = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_4_8) .maxTokens(16000L) .thinking(ThinkingConfigAdaptive.builder().build()) .addTool(weatherTool) .addUserMessage("What's the weather in Paris?") .build(); Message response = client.messages().create(initialParams); ToolUseBlock toolUseBlock = null; for (var block : response.content()) { if (block.toolUse().isPresent()) { toolUseBlock = block.toolUse().get(); break; } } int temperature = 88; // Second request: echo the assistant turn as received, then the tool result MessageCreateParams continuationParams = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_4_8) .maxTokens(16000L) .thinking(ThinkingConfigAdaptive.builder().build()) .addTool(weatherTool) .addUserMessage("What's the weather in Paris?") .addMessage(response) .addUserMessageOfBlockParams(List.of( ContentBlockParam.ofToolResult( ToolResultBlockParam.builder() .toolUseId(toolUseBlock.id()) .content("Current temperature: " + temperature + "°F") .build() ) )) .build(); Message continuation = client.messages().create(continuationParams); IO.println(continuation); } ``` ```php PHP $client = new Client(); $weatherTool = [ 'name' => 'get_weather', 'description' => 'Get current weather for a location', 'input_schema' => [ 'type' => 'object', 'properties' => [ 'location' => [ 'type' => 'string', 'description' => 'City name' ] ], 'required' => ['location'] ] ]; $response = $client->messages->create( maxTokens: 16000, messages: [ ['role' => 'user', 'content' => "What's the weather in Paris?"] ], model: 'claude-opus-4-8', thinking: ['type' => 'adaptive'], tools: [$weatherTool], ); $toolUseBlock = null; foreach ($response->content as $block) { if ($block->type === 'tool_use') { $toolUseBlock = $block; break; } } $weatherData = ['temperature' => 88]; $continuation = $client->messages->create( maxTokens: 16000, messages: [ ['role' => 'user', 'content' => "What's the weather in Paris?"], ['role' => 'assistant', 'content' => $response->content], ['role' => 'user', 'content' => [ [ 'type' => 'tool_result', 'tool_use_id' => $toolUseBlock->id, 'content' => "Current temperature: {$weatherData['temperature']}°F" ] ]] ], model: 'claude-opus-4-8', thinking: ['type' => 'adaptive'], tools: [$weatherTool], ); echo $continuation; ``` ```ruby Ruby client = Anthropic::Client.new weather_tool = { name: "get_weather", description: "Get current weather for a location", input_schema: { type: "object", properties: { location: { type: "string", description: "City name" } }, required: ["location"] } } response = client.messages.create( model: "claude-opus-4-8", max_tokens: 16000, thinking: { type: "adaptive" }, tools: [weather_tool], messages: [ { role: "user", content: "What's the weather in Paris?" } ] ) tool_use_block = response.content.find { |block| block.type == :tool_use } raise "No tool_use block found" unless tool_use_block weather_data = { temperature: 88 } continuation = client.messages.create( model: "claude-opus-4-8", max_tokens: 16000, thinking: { type: "adaptive" }, tools: [weather_tool], messages: [ { role: "user", content: "What's the weather in Paris?" }, { role: "assistant", content: response.content }, { role: "user", content: [ { type: "tool_result", tool_use_id: tool_use_block.id, content: "Current temperature: #{weather_data[:temperature]}°F" } ] } ] ) puts continuation ``` You should see Claude complete the turn with text. Because [interleaved thinking](https://platform.claude.com/docs/en/build-with-claude/thinking#interleaved-thinking) is automatic in adaptive mode, the continuation can also open with a new thinking block before the final text: ```json Output { "content": [ { "type": "text", "text": "Currently in Paris, the temperature is 88°F (31°C)" } ] } ``` ## How interleaved thinking changes the flow Interleaved thinking lets Claude think between tool calls, reasoning about each tool result before acting on it. The concept and per-model availability are covered in [Interleaved thinking](https://platform.claude.com/docs/en/build-with-claude/thinking#interleaved-thinking) on the Thinking page; interleaving changes where thinking blocks appear, not whether tool calls can chain. The following comparison shows what interleaved thinking changes in a two-tool workflow: Without interleaved thinking, Claude thinks once at the start of the assistant turn. Subsequent responses after tool results continue without new thinking blocks. ```text User: "What's the total revenue if we sold 150 units at $50 each, and how does this compare to our average monthly revenue?" Response 1: [thinking] "I need to calculate 150 * $50, then check the database..." [tool_use: calculator] { "expression": "150 * 50" } ↓ tool result: "7500" Response 2: [tool_use: database_query] { "query": "SELECT AVG(revenue)..." } ↑ no thinking block ↓ tool result: "5200" Response 3: [text] "The total revenue is $7,500, which is 44% above your average monthly revenue of $5,200." ↑ no thinking block ``` With interleaved thinking enabled, Claude can think after receiving each tool result, allowing it to reason about intermediate results before continuing. ```text User: "What's the total revenue if we sold 150 units at $50 each, and how does this compare to our average monthly revenue?" Response 1: [thinking] "I need to calculate 150 * $50 first..." [tool_use: calculator] { "expression": "150 * 50" } ↓ tool result: "7500" Response 2: [thinking] "Got $7,500. Now I should query the database to compare..." [tool_use: database_query] { "query": "SELECT AVG(revenue)..." } ↑ thinking after receiving calculator result ↓ tool result: "5200" Response 3: [thinking] "$7,500 vs $5,200 average - that's a 44% increase..." [text] "The total revenue is $7,500, which is 44% above your average monthly revenue of $5,200." ↑ thinking before final answer ``` ## Next steps The overview: turn thinking on, read thinking output, and review the full rules for tool use, caching, and streaming. Steer how often and how deeply Claude thinks with effort levels and prompt-based guidance. Manual thinking budgets on older models: `budget_tokens` mechanics and migration to adaptive. --- title: Troubleshooting thinking url: https://platform.claude.com/docs/en/build-with-claude/thinking-troubleshooting description: "Diagnose and fix the most common thinking failures: configuration 400 errors, empty or missing thinking blocks, max_tokens stops, and cache misses." --- For how zero data retention (ZDR) applies to this feature, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention). This page covers the most common failures when configuring thinking or round-tripping thinking blocks (sending returned thinking blocks back in later requests). The first section maps each model to its supported thinking configurations and the ones it rejects; the sections after it each start from a symptom you observe, so you can match an error message or unexpected response directly to its cause and fix. For how thinking works, see the [Thinking](https://platform.claude.com/docs/en/build-with-claude/thinking) overview. ## Configurations each model rejects Most thinking configuration errors are a mismatch between the `thinking.type` value in the request and what the model supports. On current models, thinking runs as `thinking: {type: "adaptive"}`, and on the newest it is on by default. Some earlier models instead use [extended thinking](https://platform.claude.com/docs/en/build-with-claude/extended-thinking), a legacy manual mode configured as `thinking: {type: "enabled", budget_tokens: N}`. Extended thinking (`thinking.type: "enabled"` with `budget_tokens`) is deprecated on the Claude 4.6 models (requests using it still succeed). Claude 4.7 and later models do not support it and reject requests that use it, returning a 400 error. On Claude 4.5 and earlier models that support thinking, extended thinking is the only available thinking mode. Claude Mythos Preview supports both modes. Where both modes are available, use [adaptive thinking](https://platform.claude.com/docs/en/build-with-claude/thinking) instead. The table lists what each model supports, what it defaults to, and which `thinking.type` values it rejects with a 400 error; any value not listed as rejected is accepted. | Model | Thinking types | Default | Rejected with 400 | | --------------------- | -------------------------------- | --------- | -------------------------- | | Claude Fable 5 | Adaptive only | Always on | `"enabled"`, `"disabled"` | | Claude Mythos 5 | Adaptive only | Always on | `"enabled"`, `"disabled"` | | Claude Mythos Preview | Adaptive, extended | Always on | `"disabled"` | | Claude Opus 5 | Adaptive only | On | `"enabled"`, `"disabled"`2 | | Claude Opus 4.8 | Adaptive only | Off | `"enabled"` | | Claude Opus 4.7 | Adaptive only | Off | `"enabled"` | | Claude Sonnet 5 | Adaptive only | On | `"enabled"` | | Claude Opus 4.6 | Adaptive, extended (deprecated)1 | Off | None | | Claude Sonnet 4.6 | Adaptive, extended (deprecated)1 | Off | None | | Claude Opus 4.5 | Extended only | Off | `"adaptive"` | | Claude Haiku 4.5 | Extended only | Off | `"adaptive"` | | Claude Sonnet 4.5 | Extended only | Off | `"adaptive"` | *1 `enabled` and `budget_tokens` still work on these models but are deprecated; use adaptive thinking instead.*\ *2 Claude Opus 5 accepts `"disabled"` at [effort](https://platform.claude.com/docs/en/build-with-claude/effort) `high` or below; combining it with effort `xhigh` or `max` returns a 400 error. This restriction applies to Claude Opus 5 and later models and is enforced on each request.* Models marked `Always on` cannot turn thinking off. Models marked `On` default to thinking but accept `thinking: {type: "disabled"}`. Earlier Claude 4 models (Claude Opus 4.1, Claude Sonnet 4, and Claude Opus 4) support extended thinking only; see [Model deprecations](https://platform.claude.com/docs/en/about-claude/model-deprecations) for their availability. Claude Fable 5 and Claude Mythos 5 are not available under [zero data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#model-specific-data-retention-requirements). ## A 400 error says `"thinking.type.enabled"` is not supported The request fails with a 400 error whose message reads: ```text wrap "thinking.type.enabled" is not supported for this model. Use "thinking.type.adaptive" and "output_config.effort" to control thinking behavior. ``` This happens because the model you requested has removed extended thinking (see [Configurations each model rejects](https://platform.claude.com/docs/en/build-with-claude/thinking-troubleshooting#rejected-configurations)). Switch the request to `thinking: {type: "adaptive"}` and steer thinking depth with `effort` instead of `budget_tokens`. [Migrating to adaptive thinking](https://platform.claude.com/docs/en/build-with-claude/extended-thinking#migrating-to-adaptive-thinking) walks through the conversion. ## A 400 error says `"thinking.type.disabled"` is not supported The request fails with a 400 error whose message reads: ```text wrap "thinking.type.disabled" is not supported for this model. Thinking defaults to adaptive mode when not specified; use "thinking.type.enabled" with "budget_tokens" for extended thinking. ``` This happens on models where thinking is always on: Claude Fable 5, Claude Mythos 5, and Claude Mythos Preview reject `"disabled"`. On Claude Fable 5 and Claude Mythos 5, the error text's suggestion of `"thinking.type.enabled"` does not apply either: those models reject it too. Omit the `thinking` parameter; these models think without any configuration. If your goal was to keep thinking text out of responses, use `display: "omitted"` instead of disabling thinking; see [Controlling thinking display](https://platform.claude.com/docs/en/build-with-claude/thinking#controlling-thinking-display). A 400 error on `"disabled"` can also occur on Claude Opus 5, which accepts `thinking: {type: "disabled"}` only at [effort](https://platform.claude.com/docs/en/build-with-claude/effort) `high` or below: combining it with effort `xhigh` or `max` is rejected. Lower the effort level, or leave thinking on. ## A 400 error says adaptive thinking is not supported The request fails with a 400 error whose message reads: ```text wrap adaptive thinking is not supported on this model ``` This happens because the model supports only extended thinking (see [Configurations each model rejects](https://platform.claude.com/docs/en/build-with-claude/thinking-troubleshooting#rejected-configurations)). Use `thinking: {type: "enabled", budget_tokens: N}` instead; see [Extended thinking](https://platform.claude.com/docs/en/build-with-claude/extended-thinking) for the configuration. ## A 400 error says thinking blocks cannot be modified A request that returns tool results fails with a 400 `invalid_request_error` whose message contains: ```text wrap `thinking` or `redacted_thinking` blocks in the latest assistant message cannot be modified ``` In multi-turn and tool-use conversations you send previous assistant messages, including their `thinking` and `redacted_thinking` blocks, back to the API, and the API verifies they arrive unmodified. This error happens when the assistant message you send back differs from the one the API returned, most often because your code filters content blocks by type and drops `redacted_thinking` blocks, or rebuilds the assistant message instead of echoing it. Echo the assistant turn back verbatim, thinking blocks included. See [Preserving thinking blocks](https://platform.claude.com/docs/en/build-with-claude/thinking#preserving-thinking-blocks) for the rules, and the worked round trip in [Thinking in tool and multi-turn workflows](https://platform.claude.com/docs/en/build-with-claude/thinking-tool-workflows#two-turn-tool-use-round-trip) for correct code in every SDK. ## The thinking field is empty in the response The response contains `thinking` blocks, but their `thinking` field is an empty string and only the `signature` field is populated. This happens because `display` defaults to `"omitted"` on newer models, which returns thinking blocks without their text. Set `display: "summarized"` in your thinking configuration to receive the summarized thinking text; see [Controlling thinking display](https://platform.claude.com/docs/en/build-with-claude/thinking#controlling-thinking-display) for the defaults per model. ## No thinking block appears on some turns Some responses contain no `thinking` block at all, even though thinking is configured. This is normal in adaptive mode: Claude skips thinking on requests it judges simple enough to answer directly. If you want thinking more often or more deeply, raise `effort` or steer with prompting; see [Steering how often Claude thinks](https://platform.claude.com/docs/en/build-with-claude/thinking-steering-and-cost#tuning-thinking-behavior). ## Tool calls or XML tags appear in the text output A response occasionally writes a tool call into its text instead of emitting a `tool_use` block, or includes `` or other internal XML tags in its visible text. A leaked tool call never runs, and in agentic loops the leaked text stays in the conversation history, so later turns are affected as well. This happens on Claude Opus 5 when thinking is disabled, most commonly on tool-heavy workloads such as search. System-prompt rules instructing the model not to think or not to reason increase the tag leakage. Re-enable thinking (the default) and use lower `effort` levels to control token cost instead. If your integration must keep thinking disabled, apply the prompting mitigations in [Running with thinking disabled](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/prompting-claude-opus-5#running-with-thinking-disabled). ## The response stops with `stop_reason: "max_tokens"` The response ends with `stop_reason: "max_tokens"`, often with a truncated or missing text block. This happens because thinking tokens count toward `max_tokens`, so a long thinking pass can consume the budget before the text response completes. Raise `max_tokens` to leave room for both thinking and text, or lower `effort` so Claude spends less on thinking; see [Cost control](https://platform.claude.com/docs/en/build-with-claude/thinking-steering-and-cost#cost-control) and [Thinking and the context window](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-and-the-context-window). ## Cache hits drop after changing thinking settings `cache_read_input_tokens` falls to zero on requests that previously hit the cache. This happens because the thinking configuration and the effort level (or its default) are part of the cached prompt prefix, so changing any of them starts a new prefix: switching thinking modes, changing the effort value, and changing `budget_tokens` all invalidate message cache breakpoints, and can invalidate tool and system-prompt breakpoints too, depending on where the model renders the configuration. Keep the thinking configuration and effort level constant across requests that share a conversation; setting a parameter explicitly to its default is equivalent to omitting it and does not invalidate. See [Thinking and prompt caching](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-and-prompt-caching). ## Setting effort does not change thinking You change `effort` but thinking frequency or depth stays the same. This happens because effort is the primary thinking lever only in adaptive mode. On extended-thinking-only models, thinking depth is set by `budget_tokens` instead. Adjust `budget_tokens` on those models, or check which mode your model runs in; see [Thinking and effort](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-and-effort). On Claude Opus 4.5, the one extended-thinking-only model that supports effort, effort composes with the budget; see [Budget rules and tuning](https://platform.claude.com/docs/en/build-with-claude/extended-thinking#budget-rules-and-tuning). ## Next steps The overview: what thinking is, how to configure it, and how it interacts with tools, caching, and streaming. The full error reference, including the thinking configuration 400s with their exact server messages. Convert `budget_tokens` requests to adaptive thinking with effort. ### Tools --- title: Tool use with Claude url: https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview description: Connect Claude to external tools and APIs. See where tools execute, when Claude calls them, and which tool fits your task. --- Tool use lets Claude call functions that you define or that Anthropic provides. Claude determines when to call a tool based on the user's request and the tool's description. It then returns a structured call that your application executes (client tools) or that Anthropic executes (server tools). Here's a minimal example using a server tool, the [Web search tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-search-tool), which Anthropic executes for you: ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "tools": [{"type": "web_search_20260209", "name": "web_search"}], "messages": [{"role": "user", "content": "What'\''s the latest on the Mars rover?"}] }' ``` ```bash CLI ant messages create --transform content --format yaml \ --model claude-opus-5 \ --max-tokens 1024 \ --tool '{type: web_search_20260209, name: web_search}' \ --message '{role: user, content: "What is the latest on the Mars rover?"}' ``` ```python Python client = anthropic.Anthropic() response = client.messages.create( model="claude-opus-5", max_tokens=1024, tools=[{"type": "web_search_20260209", "name": "web_search"}], messages=[{"role": "user", "content": "What's the latest on the Mars rover?"}], ) print(response.content) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, tools: [{ type: "web_search_20260209", name: "web_search" }], messages: [{ role: "user", content: "What's the latest on the Mars rover?" }] }); console.log(response.content); ``` ```csharp C# AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Tools = [new ToolUnion(new WebSearchTool20260209())], Messages = [new() { Role = Role.User, Content = "What's the latest on the Mars rover?" }] }; var message = await client.Messages.Create(parameters); Console.WriteLine(message.Content); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Tools: []anthropic.ToolUnionParam{ {OfWebSearchTool20260209: &anthropic.WebSearchTool20260209Param{}}, }, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("What's the latest on the Mars rover?")), }, }) if err != nil { log.Fatal(err) } fmt.Println(response.Content) ``` ```java Java import com.anthropic.models.messages.WebSearchTool20260209; void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addTool(WebSearchTool20260209.builder().build()) .addUserMessage("What's the latest on the Mars rover?") .build(); Message response = client.messages().create(params); IO.println(response.content()); } ``` ```php PHP $client = new Client(); $message = $client->messages->create( model: 'claude-opus-5', maxTokens: 1024, tools: [ ['type' => 'web_search_20260209', 'name' => 'web_search'], ], messages: [ ['role' => 'user', 'content' => "What's the latest on the Mars rover?"], ], ); echo $message; ``` ```ruby Ruby client = Anthropic::Client.new message = client.messages.create( model: "claude-opus-5", max_tokens: 1024, tools: [{ type: "web_search_20260209", name: "web_search" }], messages: [{ role: "user", content: "What's the latest on the Mars rover?" }] ) puts message.content ``` Claude runs the search on Anthropic's infrastructure and returns the cited results in the same response. To have Claude call a function that you define, pass a tool with an `input_schema`, then execute the call when Claude returns a `tool_use` block. [How tool use works](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview#how-tool-use-works) shows that round trip end to end. Learn more about [defining tools](https://platform.claude.com/docs/en/agents-and-tools/tool-use/define-tools) and [handling tool calls](https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls). ## How tool use works Tools differ primarily by where the code executes. **Client tools** (including user-defined tools and tools with Anthropic-defined schemas, such as `bash` and `text_editor`) run in your application. Claude responds with `stop_reason: "tool_use"` and one or more `tool_use` blocks. Your code executes the operation and sends back a `tool_result`. **Server tools** (such as `web_search`, `web_fetch`, `code_execution`, and `tool_search`) run on Anthropic's infrastructure: you see the results directly without handling execution, unless Claude calls the tool in the same group of parallel tool calls as one of your client tools (see [Stop reasons and fallback](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons#tool-use)). Here's that round trip in full for a client tool. The first request defines a `get_weather` tool, and Claude answers the question by calling it: the response carries a `tool_use` block, your code runs the lookup, and a second request sends the result back in a `tool_result` block so Claude can reply with the answer. ```bash cURL # Claude replies with a tool_use block naming the tool and its arguments. TOOLS='[ { "name": "get_weather", "description": "Get the current weather for a given location.", "input_schema": { "type": "object", "properties": { "location": {"type": "string", "description": "City and state, e.g. San Francisco, CA"} }, "required": ["location"] } } ]' USER_MSG="What's the weather in San Francisco?" RESPONSE=$(curl -s https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d "$(jq -n --argjson tools "$TOOLS" --arg msg "$USER_MSG" '{ model: "claude-opus-5", max_tokens: 1024, tools: $tools, # Ask for at most one tool call per turn. tool_choice: {type: "auto", disable_parallel_tool_use: true}, messages: [{role: "user", content: $msg}] }')") TOOL_USE=$(echo "$RESPONSE" | jq '.content[] | select(.type == "tool_use")') echo "Claude called $(echo "$TOOL_USE" | jq -r '.name') with $(echo "$TOOL_USE" | jq -c '.input')" # Run the tool, then send the result back in a tool_result block. WEATHER="15 degrees Celsius, partly cloudy" FOLLOWUP=$(curl -s https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d "$(jq -n \ --argjson tools "$TOOLS" \ --arg msg "$USER_MSG" \ --argjson assistant "$(echo "$RESPONSE" | jq '.content')" \ --arg tool_use_id "$(echo "$TOOL_USE" | jq -r '.id')" \ --arg weather "$WEATHER" \ '{ model: "claude-opus-5", max_tokens: 1024, tools: $tools, tool_choice: {type: "auto", disable_parallel_tool_use: true}, messages: [ {role: "user", content: $msg}, {role: "assistant", content: $assistant}, {role: "user", content: [ {type: "tool_result", tool_use_id: $tool_use_id, content: $weather} ]} ] }')") # Claude uses the result to answer the original question. echo "$FOLLOWUP" | jq -r '.content[] | select(.type == "text") | .text' ``` ```bash CLI # ant reads the request body as YAML on stdin; jq carries the conversation # state into the second request. USER_MSG="What's the weather in San Francisco?" MESSAGES=$(jq -n --arg msg "$USER_MSG" '[{role: "user", content: $msg}]') call_api() { { cat <<'YAML' model: claude-opus-5 max_tokens: 1024 # Ask for at most one tool call per turn. tool_choice: {type: auto, disable_parallel_tool_use: true} tools: - name: get_weather description: Get the current weather for a given location. input_schema: type: object properties: location: {type: string, description: "City and state, e.g. San Francisco, CA"} required: [location] YAML printf 'messages: %s\n' "$MESSAGES" } | ant messages create --format json } # Claude replies with a tool_use block naming the tool and its arguments. RESPONSE=$(call_api) TOOL_USE=$(jq '.content[] | select(.type == "tool_use")' <<<"$RESPONSE") echo "Claude called $(jq -r '.name' <<<"$TOOL_USE") with $(jq -c '.input' <<<"$TOOL_USE")" # Run the tool, then send the result back in a tool_result block. WEATHER="15 degrees Celsius, partly cloudy" MESSAGES=$(jq \ --argjson assistant "$(jq '.content' <<<"$RESPONSE")" \ --arg tool_use_id "$(jq -r '.id' <<<"$TOOL_USE")" \ --arg weather "$WEATHER" \ '. + [ {role: "assistant", content: $assistant}, {role: "user", content: [ {type: "tool_result", tool_use_id: $tool_use_id, content: $weather} ]} ]' <<<"$MESSAGES") FOLLOWUP=$(call_api) # Claude uses the result to answer the original question. jq -r '.content[] | select(.type == "text") | .text' <<<"$FOLLOWUP" ``` ```python Python client = anthropic.Anthropic() tools = [ { "name": "get_weather", "description": "Get the current weather for a given location.", "input_schema": { "type": "object", "properties": { "location": { "type": "string", "description": "City and state, e.g. San Francisco, CA", } }, "required": ["location"], }, } ] messages = [{"role": "user", "content": "What's the weather in San Francisco?"}] # Claude replies with a tool_use block naming the tool and its arguments. response = client.messages.create( model="claude-opus-5", max_tokens=1024, tools=tools, # Ask for at most one tool call per turn. tool_choice={"type": "auto", "disable_parallel_tool_use": True}, messages=messages, ) tool_use = next(block for block in response.content if block.type == "tool_use") print(f"Claude called {tool_use.name} with {json.dumps(tool_use.input)}") # Run the tool, then send the result back in a tool_result block. weather = "15 degrees Celsius, partly cloudy" # your weather lookup goes here messages += [ {"role": "assistant", "content": response.content}, { "role": "user", "content": [ {"type": "tool_result", "tool_use_id": tool_use.id, "content": weather} ], }, ] followup = client.messages.create( model="claude-opus-5", max_tokens=1024, tools=tools, tool_choice={"type": "auto", "disable_parallel_tool_use": True}, messages=messages, ) # Claude uses the result to answer the original question. final_text = next(block for block in followup.content if block.type == "text") print(final_text.text) ``` ```typescript TypeScript const client = new Anthropic(); const tools: Anthropic.Tool[] = [ { name: "get_weather", description: "Get the current weather for a given location.", input_schema: { type: "object", properties: { location: { type: "string", description: "City and state, e.g. San Francisco, CA" } }, required: ["location"] } } ]; const messages: Anthropic.MessageParam[] = [ { role: "user", content: "What's the weather in San Francisco?" } ]; // Claude replies with a tool_use block naming the tool and its arguments. const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, tools, // Ask for at most one tool call per turn. tool_choice: { type: "auto", disable_parallel_tool_use: true }, messages }); const toolUse = response.content.find( (block): block is Anthropic.ToolUseBlock => block.type === "tool_use" )!; console.log(`Claude called ${toolUse.name} with ${JSON.stringify(toolUse.input)}`); // Run the tool, then send the result back in a tool_result block. const weather = "15 degrees Celsius, partly cloudy"; // your weather lookup goes here messages.push( { role: "assistant", content: response.content }, { role: "user", content: [{ type: "tool_result", tool_use_id: toolUse.id, content: weather }] } ); const followup = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, tools, tool_choice: { type: "auto", disable_parallel_tool_use: true }, messages }); // Claude uses the result to answer the original question. const finalText = followup.content.find( (block): block is Anthropic.TextBlock => block.type === "text" )!; console.log(finalText.text); ``` ```csharp C# AnthropicClient client = new(); List tools = [ new ToolUnion(new Tool() { Name = "get_weather", Description = "Get the current weather for a given location.", InputSchema = new InputSchema() { Properties = new Dictionary { ["location"] = JsonSerializer.SerializeToElement(new { type = "string", description = "City and state, e.g. San Francisco, CA", }), }, Required = ["location"], }, }), ]; // Ask for at most one tool call per turn. var toolChoice = new ToolChoice(new ToolChoiceAuto { DisableParallelToolUse = true }); const string userPrompt = "What's the weather in San Francisco?"; // Claude replies with a tool_use block naming the tool and its arguments. var response = await client.Messages.Create(new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Tools = tools, ToolChoice = toolChoice, Messages = [new() { Role = Role.User, Content = userPrompt }], }); ToolUseBlock? toolUse = null; foreach (var block in response.Content) { if (block.TryPickToolUse(out var picked)) { toolUse = picked; break; } } Console.WriteLine($"Claude called {toolUse!.Name} with {JsonSerializer.Serialize(toolUse.Input)}"); // Run the tool, then send the result back in a tool_result block. var weather = "15 degrees Celsius, partly cloudy"; List toolResults = [ new ContentBlockParam(new ToolResultBlockParam() { ToolUseID = toolUse.ID, Content = weather, }), ]; var followup = await client.Messages.Create(new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Tools = tools, ToolChoice = toolChoice, Messages = [ new() { Role = Role.User, Content = userPrompt }, new() { Role = Role.Assistant, Content = response.Content.Select(block => new ContentBlockParam(block.Json)).ToList() }, new() { Role = Role.User, Content = new MessageParamContent(toolResults) }, ], }); // Claude uses the result to answer the original question. foreach (var block in followup.Content) { if (block.TryPickText(out var text)) { Console.WriteLine(text.Text); } } ``` ```go Go client := anthropic.NewClient() ctx := context.Background() tools := []anthropic.ToolUnionParam{ {OfTool: &anthropic.ToolParam{ Name: "get_weather", Description: anthropic.String("Get the current weather for a given location."), InputSchema: anthropic.ToolInputSchemaParam{ Properties: map[string]any{ "location": map[string]any{ "type": "string", "description": "City and state, e.g. San Francisco, CA", }, }, Required: []string{"location"}, }, }}, } // Ask for at most one tool call per turn. toolChoice := anthropic.ToolChoiceUnionParam{ OfAuto: &anthropic.ToolChoiceAutoParam{DisableParallelToolUse: anthropic.Bool(true)}, } messages := []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("What's the weather in San Francisco?")), } // Claude replies with a tool_use block naming the tool and its arguments. response, err := client.Messages.New(ctx, anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Tools: tools, ToolChoice: toolChoice, Messages: messages, }) if err != nil { log.Fatal(err) } var toolUse anthropic.ContentBlockUnion for _, block := range response.Content { if block.Type == "tool_use" { toolUse = block break } } fmt.Printf("Claude called %s with %s\n", toolUse.Name, string(toolUse.Input)) // Run the tool, then send the result back in a tool_result block. weather := "15 degrees Celsius, partly cloudy" var assistantContent []anthropic.ContentBlockParamUnion for _, block := range response.Content { assistantContent = append(assistantContent, block.ToParam()) } messages = append(messages, anthropic.NewAssistantMessage(assistantContent...), anthropic.NewUserMessage(anthropic.NewToolResultBlock(toolUse.ID, weather, false)), ) followup, err := client.Messages.New(ctx, anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Tools: tools, ToolChoice: toolChoice, Messages: messages, }) if err != nil { log.Fatal(err) } // Claude uses the result to answer the original question. for _, block := range followup.Content { if block.Type == "text" { fmt.Println(block.Text) } } ``` ```java Java import com.anthropic.core.JsonValue; import com.anthropic.models.messages.ContentBlockParam; // ... import com.anthropic.models.messages.Tool; import com.anthropic.models.messages.Tool.InputSchema; import com.anthropic.models.messages.ToolChoiceAuto; import com.anthropic.models.messages.ToolResultBlockParam; import com.anthropic.models.messages.ToolUseBlock; // ... void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); Tool weatherTool = Tool.builder() .name("get_weather") .description("Get the current weather for a given location.") .inputSchema(InputSchema.builder() .properties(JsonValue.from(Map.of( "location", Map.of( "type", "string", "description", "City and state, e.g. San Francisco, CA" ) ))) .required(List.of("location")) .build()) .build(); // Ask for at most one tool call per turn. ToolChoiceAuto toolChoice = ToolChoiceAuto.builder() .disableParallelToolUse(true) .build(); String userPrompt = "What's the weather in San Francisco?"; // Claude replies with a tool_use block naming the tool and its arguments. Message response = client.messages().create(MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addTool(weatherTool) .toolChoice(toolChoice) .addUserMessage(userPrompt) .build()); ToolUseBlock toolUse = response.content().stream() .flatMap(block -> block.toolUse().stream()) .findFirst() .orElseThrow(); IO.println("Claude called " + toolUse.name() + " with " + toolUse._input()); // Run the tool, then send the result back in a tool_result block. String weather = "15 degrees Celsius, partly cloudy"; Message followup = client.messages().create(MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addTool(weatherTool) .toolChoice(toolChoice) .addUserMessage(userPrompt) .addMessage(response) .addUserMessageOfBlockParams(List.of(ContentBlockParam.ofToolResult( ToolResultBlockParam.builder() .toolUseId(toolUse.id()) .content(weather) .build()))) .build()); // Claude uses the result to answer the original question. followup.content().stream() .flatMap(block -> block.text().stream()) .forEach(textBlock -> IO.println(textBlock.text())); } ``` ```php PHP use Anthropic\Messages\ToolChoiceAuto; $client = new Client(); $tools = [ [ 'name' => 'get_weather', 'description' => 'Get the current weather for a given location.', 'input_schema' => [ 'type' => 'object', 'properties' => [ 'location' => [ 'type' => 'string', 'description' => 'City and state, e.g. San Francisco, CA', ], ], 'required' => ['location'], ], ], ]; $userMessage = ['role' => 'user', 'content' => "What's the weather in San Francisco?"]; // Ask for at most one tool call per turn. $toolChoice = ToolChoiceAuto::with(disableParallelToolUse: true); // Claude replies with a tool_use block naming the tool and its arguments. $response = $client->messages->create( model: 'claude-opus-5', maxTokens: 1024, tools: $tools, toolChoice: $toolChoice, messages: [$userMessage], ); $toolUse = null; foreach ($response->content as $block) { if ($block->type === 'tool_use') { $toolUse = $block; break; } } printf("Claude called %s with %s\n", $toolUse->name, json_encode($toolUse->input)); // Run the tool, then send the result back in a tool_result block. $weather = '15 degrees Celsius, partly cloudy'; $followup = $client->messages->create( model: 'claude-opus-5', maxTokens: 1024, tools: $tools, toolChoice: $toolChoice, messages: [ $userMessage, ['role' => 'assistant', 'content' => $response->content], [ 'role' => 'user', 'content' => [ [ 'type' => 'tool_result', 'tool_use_id' => $toolUse->id, 'content' => $weather, ], ], ], ], ); // Claude uses the result to answer the original question. foreach ($followup->content as $block) { if ($block->type === 'text') { echo $block->text, "\n"; } } ``` ```ruby Ruby client = Anthropic::Client.new tools = [ { name: "get_weather", description: "Get the current weather for a given location.", input_schema: { type: "object", properties: { location: {type: "string", description: "City and state, e.g. San Francisco, CA"} }, required: ["location"] } } ] messages = [{role: "user", content: "What's the weather in San Francisco?"}] # Claude replies with a tool_use block naming the tool and its arguments. response = client.messages.create( model: "claude-opus-5", max_tokens: 1024, tools: tools, # Ask for at most one tool call per turn. tool_choice: {type: "auto", disable_parallel_tool_use: true}, messages: messages ) tool_use = response.content.find { |block| block.type == :tool_use } puts "Claude called #{tool_use.name} with #{JSON.generate(tool_use.input)}" # Run the tool, then send the result back in a tool_result block. weather = "15 degrees Celsius, partly cloudy" messages += [ {role: "assistant", content: response.content}, { role: "user", content: [ {type: "tool_result", tool_use_id: tool_use.id, content: weather} ] } ] followup = client.messages.create( model: "claude-opus-5", max_tokens: 1024, tools: tools, tool_choice: {type: "auto", disable_parallel_tool_use: true}, messages: messages ) # Claude uses the result to answer the original question. final_text = followup.content.find { |block| block.type == :text } puts final_text.text ``` ```text Output wrap Claude called get_weather with {"location": "San Francisco, CA"} The current weather in San Francisco is 15 degrees Celsius with partly cloudy skies. ``` [Handle tool calls](https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls) covers each step in detail, including result formatting and error signaling; [Parallel tool use](https://platform.claude.com/docs/en/agents-and-tools/tool-use/parallel-tool-use) covers responses that call several tools at once. To skip writing this round trip yourself, use [Tool Runner](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-runner): the SDKs execute your tools and send the results back automatically. For the full conceptual model including the agentic loop and when to choose each approach, see [How tool use works](https://platform.claude.com/docs/en/agents-and-tools/tool-use/how-tool-use-works). For connecting to Model Context Protocol (MCP) servers, see the [MCP connector](https://platform.claude.com/docs/en/agents-and-tools/mcp-connector). For building your own MCP client, see the Model Context Protocol guide to [building an MCP client](https://modelcontextprotocol.io/docs/develop/build-client). ## When Claude uses tools With the default `tool_choice` of `{"type": "auto"}`, Claude determines on each turn whether to call a tool or respond directly. It calls a tool when the request maps to that tool's described capability and the answer isn't already in context. It responds directly for stable knowledge, creative tasks, and conversational turns. This boundary is steerable through your system prompt. If Claude isn't calling tools when you expect, a light instruction such as `"Use the tools to investigate before responding."` increases tool use. A stronger form such as `"Always call a tool first before responding."` pushes further. Conversely, `"Use your judgment about whether to call a tool or respond directly."` keeps triggering behavior conservative. To require a tool call rather than rely on prompting, set [`tool_choice`](https://platform.claude.com/docs/en/agents-and-tools/tool-use/define-tools#forcing-tool-use). **Guarantee schema conformance with strict tool use** Add `strict: true` to your custom tool definitions to ensure Claude's tool calls always match your schema exactly. See [Strict tool use](https://platform.claude.com/docs/en/agents-and-tools/tool-use/strict-tool-use). Each server tool's page describes its own trigger boundary in more detail. If the user's prompt doesn't include enough information to fill all the required parameters for a tool, Claude Opus is much more likely to recognize that a parameter is missing and ask for it. Claude Sonnet might ask, especially when prompted to think before outputting a tool request. But it might also infer a reasonable value. For example, given a `get_weather` tool that requires a `location` parameter, if you ask Claude "What's the weather?" without specifying a location, Claude (particularly Claude Sonnet) might guess values you didn't supply: ```json JSON { "type": "tool_use", "id": "toolu_01A09q90qw90lq917835lq9", "name": "get_weather", "input": { "location": "New York, NY", "unit": "fahrenheit" } } ``` This behavior is not guaranteed, especially for more ambiguous prompts and for less capable models. ## Choose a tool For `type` strings, versions, and beta headers, see [Tool reference](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-reference). ### Your own tools For tools you define, you write the schema and your application executes each call. Specify tool schemas, write descriptions, and control when Claude calls your tools. Parse `tool_use` blocks, format `tool_result` responses, and handle errors. ### Anthropic-schema client tools Anthropic publishes the schema and trains Claude on it. Your application still executes each call and returns the `tool_result`. Store and retrieve information across conversations in files you control. Run shell commands in a persistent session that maintains state. View and modify text files to debug, fix, and improve code. Take screenshots and control the mouse and keyboard in a desktop environment. ### Server tools Server tools run on Anthropic's infrastructure, with no handler code in your application. See [Server tools](https://platform.claude.com/docs/en/agents-and-tools/tool-use/server-tools) for the mechanics they share. Search the web for information beyond the knowledge cutoff, with cited sources. Retrieve the full content of specified web pages and PDF documents. Run Python and bash code in a sandboxed container to analyze data and generate files. Let a faster executor model consult a higher-intelligence advisor model mid-generation. Work with thousands of tools by discovering and loading them on demand. Connect to remote MCP servers from the Messages API without a separate MCP client. [Claude Managed Agents](https://platform.claude.com/docs/en/managed-agents/overview) provides a built-in toolset that Claude uses autonomously within a session. For that toolset and the Managed Agents way to add custom tools, see its [Tools](https://platform.claude.com/docs/en/managed-agents/tools) page. ## Pricing Tool use requests are priced based on: 1. The total number of input tokens sent to the model (including in the `tools` parameter) 2. The number of output tokens generated 3. For server-side tools, additional usage-based pricing (for example, web search charges per search performed) Client-side tools are priced the same as any other Claude API request, although server-side tools can incur additional charges based on their specific usage. The additional tokens from tool use come from: * The `tools` parameter in API requests (tool names, descriptions, and schemas) * `tool_use` content blocks in API requests and responses * `tool_result` content blocks in API requests When you use `tools`, the API also automatically includes a special system prompt for the model that enables tool use. The number of tool use tokens required for each model is listed in the following table (excluding the additional tokens listed earlier). Note that the table assumes at least 1 tool is provided. If no `tools` are provided, then a tool choice of `none` uses 0 additional system prompt tokens. | Model | Tool choice | Tool use system prompt token count | | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | ---------------------------------- | | Claude Opus 5 | `auto`, `none`***`any`, `tool` | 286 tokens***406 tokens | | Claude Opus 4.8 | `auto`, `none`***`any`, `tool` | 290 tokens***410 tokens | | Claude Opus 4.7 | `auto`, `none`***`any`, `tool` | 675 tokens***804 tokens | | Claude Opus 4.6 | `auto`, `none`***`any`, `tool` | 497 tokens***589 tokens | | Claude Opus 4.5 | `auto`, `none`***`any`, `tool` | 496 tokens***588 tokens | | Claude Opus 4.1 ([retired, except on Bedrock and Google Cloud](https://platform.claude.com/docs/en/about-claude/model-deprecations)) | `auto`, `none`***`any`, `tool` | 313 tokens***315 tokens | | Claude Opus 4 ([retired, except on Google Cloud](https://platform.claude.com/docs/en/about-claude/model-deprecations)) | `auto`, `none`***`any`, `tool` | 313 tokens***315 tokens | | Claude Sonnet 5 | `auto`, `none`***`any`, `tool` | 354 tokens***474 tokens | | Claude Sonnet 4.6 | `auto`, `none`***`any`, `tool` | 497 tokens***589 tokens | | Claude Sonnet 4.5 | `auto`, `none`***`any`, `tool` | 496 tokens***588 tokens | | Claude Sonnet 4 ([retired, except on Bedrock and Google Cloud](https://platform.claude.com/docs/en/about-claude/model-deprecations)) | `auto`, `none`***`any`, `tool` | 313 tokens***315 tokens | | Claude Haiku 4.5 | `auto`, `none`***`any`, `tool` | 496 tokens***588 tokens | | Claude Haiku 3.5 ([retired, except on Bedrock and Google Cloud](https://platform.claude.com/docs/en/about-claude/model-deprecations)) | `auto`, `none`***`any`, `tool` | 264 tokens***355 tokens | These token counts are added to your normal input and output tokens to calculate the total cost of a request. See the [Models overview](https://platform.claude.com/docs/en/about-claude/models/overview#latest-models-comparison) table for current per-model prices. When you send a tool use prompt, like any other API request, the response includes both input and output token counts in the reported `usage` metrics. Some server tools add usage-based charges on top of tokens: see [Web search tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-search-tool#usage-and-pricing) and [Code execution tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool#usage-and-pricing) for their rates. ## Next steps Understand the tool use loop, where tools execute, and when to use tools instead of prose. A guided walkthrough from a single tool call to a production-ready agentic loop. Directory of Anthropic-provided tools and reference for optional tool definition properties. --- title: Advisor tool url: https://platform.claude.com/docs/en/agents-and-tools/tool-use/advisor-tool description: Pair a faster executor model with a higher-intelligence advisor model that provides strategic guidance mid-generation. --- The advisor tool lets a faster, lower-cost **executor model** consult a higher-intelligence **advisor model** mid-generation for strategic guidance. The advisor reads the full conversation, produces a plan or course correction, and the executor continues with the task. This pattern fits long-horizon agentic workloads (coding agents, computer use, multistep research pipelines) where most turns are mechanical but having an excellent plan is crucial. You get close to advisor-solo quality while the bulk of token generation happens at executor-model rates. For measured results, including how the benefit shrinks as the executor's own capability approaches the advisor's, see [Optimizing for cost and intelligence](https://platform.claude.com/docs/en/about-claude/models/optimizing-for-cost-and-intelligence). ```mermaid sequenceDiagram participant U as Your application participant E as Executor model participant A as Advisor model U->>E: Request with advisor tool note over E: Executor begins the task E->>A: server_tool_use (server-side) note over A: Reads the full transcript,
returns strategic guidance A-->>E: advisor_tool_result note over E: Executor continues,
informed by the advice E-->>U: Response ``` For how zero data retention (ZDR) applies to this feature, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention). ## When to use it The advisor fits these configurations: * **You currently use Sonnet on complex tasks:** Add a higher-tier advisor. Opus keeps total cost similar or lower; Claude Fable 5 maximizes the quality lift. * **You currently use Haiku and want a step up in intelligence:** Add an Opus or Fable advisor. Expect higher cost than Haiku alone, but lower than switching the executor to a larger model. Results are task-dependent. Evaluate on your own workload. The advisor is a weaker fit for single-turn Q\&A (nothing to plan), pure pass-through model pickers where your users already choose their own cost and quality tradeoff, or workloads where every turn genuinely requires the advisor model's full capability. ## Quick start The advisor tool is in beta. Include the beta header `advisor-tool-2026-03-01` in your requests. ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: advisor-tool-2026-03-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-sonnet-5", "max_tokens": 4096, "tools": [ { "type": "advisor_20260301", "name": "advisor", "model": "claude-opus-5" } ], "messages": [{ "role": "user", "content": "Build a concurrent worker pool in Go with graceful shutdown." }] }' ``` ```bash CLI ant beta:messages create --beta advisor-tool-2026-03-01 <<'YAML' model: claude-sonnet-5 max_tokens: 4096 tools: - type: advisor_20260301 name: advisor model: claude-opus-5 messages: - role: user content: Build a concurrent worker pool in Go with graceful shutdown. YAML ``` ```python Python client = anthropic.Anthropic() response = client.beta.messages.create( model="claude-sonnet-5", max_tokens=4096, betas=["advisor-tool-2026-03-01"], tools=[ { "type": "advisor_20260301", "name": "advisor", "model": "claude-opus-5", } ], messages=[ { "role": "user", "content": "Build a concurrent worker pool in Go with graceful shutdown.", } ], ) print(response) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.beta.messages.create({ model: "claude-sonnet-5", max_tokens: 4096, betas: ["advisor-tool-2026-03-01"], tools: [ { type: "advisor_20260301", name: "advisor", model: "claude-opus-5" } ], messages: [ { role: "user", content: "Build a concurrent worker pool in Go with graceful shutdown." } ] }); console.log(response); ``` ```csharp C# using Anthropic.Models.Beta.Messages; using Messages = Anthropic.Models.Messages; var client = new AnthropicClient(); var parameters = new MessageCreateParams { Model = Messages::Model.ClaudeSonnet5, MaxTokens = 4096, Tools = new BetaToolUnion[] { new BetaAdvisorTool20260301 { Model = Messages::Model.ClaudeOpus5 } }, Messages = [ new BetaMessageParam { Role = Role.User, Content = "Build a concurrent worker pool in Go with graceful shutdown." } ], Betas = ["advisor-tool-2026-03-01"] }; var response = await client.Beta.Messages.Create(parameters); Console.WriteLine(response); ``` ```go Go client := anthropic.NewClient() response, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeSonnet5, MaxTokens: 4096, Tools: []anthropic.BetaToolUnionParam{ {OfAdvisorTool20260301: &anthropic.BetaAdvisorTool20260301Param{ Model: anthropic.ModelClaudeOpus5, }}, }, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Build a concurrent worker pool in Go with graceful shutdown.")), }, Betas: []anthropic.AnthropicBeta{ anthropic.AnthropicBetaAdvisorTool2026_03_01, }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java import com.anthropic.models.beta.messages.BetaAdvisorTool20260301; import com.anthropic.models.beta.messages.BetaMessage; import com.anthropic.models.beta.messages.MessageCreateParams; import com.anthropic.models.messages.Model; void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_SONNET_5) .maxTokens(4096L) .addTool(BetaAdvisorTool20260301.builder() .model(Model.CLAUDE_OPUS_5) .build()) .addUserMessage("Build a concurrent worker pool in Go with graceful shutdown.") .addBeta("advisor-tool-2026-03-01") .build(); BetaMessage response = client.beta().messages().create(params); IO.println(response); } ``` ```php PHP $client = new Client(); $response = $client->beta->messages->create( maxTokens: 4096, messages: [ [ 'role' => 'user', 'content' => 'Build a concurrent worker pool in Go with graceful shutdown.', ], ], model: 'claude-sonnet-5', tools: [ [ 'type' => 'advisor_20260301', 'name' => 'advisor', 'model' => 'claude-opus-5', ], ], betas: ['advisor-tool-2026-03-01'], ); echo $response; ``` ```ruby Ruby client = Anthropic::Client.new response = client.beta.messages.create( model: "claude-sonnet-5", max_tokens: 4096, tools: [ { type: "advisor_20260301", name: "advisor", model: "claude-opus-5" } ], messages: [ { role: "user", content: "Build a concurrent worker pool in Go with graceful shutdown." } ], betas: ["advisor-tool-2026-03-01"] ) puts response ``` The response `content` includes an `advisor_tool_result` block carrying the advisor's guidance. With `claude-opus-5` as the advisor, as in this quick start, the block's `content` field is an `advisor_redacted_result` variant (encrypted; the executor reads it server-side, but your client does not). To see the advice text directly in your response, use `claude-opus-4-8` as the advisor model instead, which returns the plaintext `advisor_result` variant. See [Result variants](https://platform.claude.com/docs/en/agents-and-tools/tool-use/advisor-tool#result-variants) for both shapes side by side and which advisor models return which, and [Model compatibility](https://platform.claude.com/docs/en/agents-and-tools/tool-use/advisor-tool#model-compatibility) for the full list of valid pairs. ## How it works When you add the advisor tool to your `tools` array, the executor model determines when to call it, like any other tool. When the executor calls the advisor: 1. The executor emits a [`server_tool_use`](https://platform.claude.com/docs/en/agents-and-tools/tool-use/server-tools) block with `name: "advisor"` and an empty `input`. The executor signals timing, and the server supplies context. 2. Anthropic runs a separate inference pass on the advisor model server-side. The advisor runs under its own Anthropic-supplied system prompt and receives the executor's full transcript as quoted context in its input. That transcript includes your system prompt, the tool definitions, the prior turns and tool results, and the text the executor has produced so far in this turn. 3. The advisor's response returns to the executor as an `advisor_tool_result` block. 4. The executor continues generating, informed by the advice. All of this occurs inside a single `/v1/messages` request, with no extra round trips on your side. The exception is a turn that pauses mid-call, which you resume with a follow-up request (see [Resuming a paused turn](https://platform.claude.com/docs/en/agents-and-tools/tool-use/advisor-tool#resuming-a-paused-turn)). The advisor itself runs without tools and without context management. Its thinking blocks are dropped before the result returns. Only the advice text reaches the executor. ## Tool parameters | Parameter | Type | Default | Description | | ------------ | -------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `type` | string | *required* | Must be `"advisor_20260301"`. | | `name` | string | *required* | Must be `"advisor"`. | | `model` | string | *required* | The advisor model ID, such as claude-opus-5. Billed at this model's rates for the sub-inference. | | `max_uses` | integer | unlimited | Maximum number of advisor calls allowed in a single request. Once the executor reaches this cap, further advisor calls return an `advisor_tool_result_error` with `error_code: "max_uses_exceeded"` and the executor continues without further advice. This is a per-request cap, not a per-conversation cap. See [Cost control](https://platform.claude.com/docs/en/agents-and-tools/tool-use/advisor-tool#cost-control) for conversation-level limits. | | `max_tokens` | integer | advisor model's output cap | Caps the advisor's total output (thinking plus text) per call. Minimum 1024. See [Capping advisor output](https://platform.claude.com/docs/en/agents-and-tools/tool-use/advisor-tool#capping-advisor-output). | | `caching` | object \| null | `null` (off) | Enables [prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) for the advisor's own transcript across calls within a conversation. See [Advisor prompt caching](https://platform.claude.com/docs/en/agents-and-tools/tool-use/advisor-tool#advisor-prompt-caching). | The `caching` object has the shape `{"type": "ephemeral", "ttl": "5m" | "1h"}`. Unlike `cache_control` on content blocks, this is not a breakpoint marker. It is an on/off switch. The server determines where cache boundaries go. The advisor tool also accepts the generic properties available on any tool definition: `cache_control`, `allowed_callers`, `defer_loading`, and `strict` (covered in [structured outputs](https://platform.claude.com/docs/en/build-with-claude/structured-outputs)). See the [Tool reference](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-reference#tool-definition-properties) for their semantics. ## Response structure ### Successful advisor call When the advisor is called, a `server_tool_use` block is followed by an `advisor_tool_result` block in the assistant's content. The following example shows the plaintext `advisor_result` variant returned by a Claude Opus 4.8 advisor. The [Quick start](https://platform.claude.com/docs/en/agents-and-tools/tool-use/advisor-tool#quick-start) uses Claude Opus 5, which returns the encrypted `advisor_redacted_result` variant instead; see [Result variants](https://platform.claude.com/docs/en/agents-and-tools/tool-use/advisor-tool#result-variants) for both shapes side by side. ```json { "role": "assistant", "content": [ { "type": "text", "text": "Let me consult the advisor on this." }, { "type": "server_tool_use", "id": "srvtoolu_abc123", "name": "advisor", "input": {} }, { "type": "advisor_tool_result", "tool_use_id": "srvtoolu_abc123", "content": { "type": "advisor_result", "text": "Use a channel-based coordination pattern. The tricky part is draining in-flight work during shutdown: close the input channel first, then wait on a WaitGroup..." } }, { "type": "text", "text": "Here's the implementation. I'm using a channel-based coordination pattern to avoid writer starvation..." } ] } ``` The `server_tool_use.input` is always empty. The server constructs the advisor's view from the full transcript automatically. Nothing the executor puts in `input` reaches the advisor. ### Result variants The `advisor_tool_result.content` field is a discriminated union. For successful calls, the variant depends on the advisor model: | Variant | Fields | Returned when | | ------------------------- | ---------------------------------- | ------------------------------------------------------------------- | | `advisor_result` | `text`, `stop_reason` | The advisor model returns plaintext (for example, Claude Opus 4.8). | | `advisor_redacted_result` | `encrypted_content`, `stop_reason` | The advisor model returns encrypted output. | Currently, Claude Opus 5, Claude Fable 5, and Claude Mythos 5 advisors return the encrypted `advisor_redacted_result`. Every other advisor model in the [compatibility table](https://platform.claude.com/docs/en/agents-and-tools/tool-use/advisor-tool#model-compatibility) returns the plaintext `advisor_result`. To read the advice text in your own responses, use an advisor that returns plaintext, such as `claude-opus-4-8`. Here is the same request sent twice, identical except for the advisor `model` in the tool definition, showing both variants. With `"model": "claude-opus-4-8"`, the advice is plaintext: ```json { "type": "advisor_tool_result", "tool_use_id": "srvtoolu_abc123", "content": { "type": "advisor_result", "text": "Use a channel-based coordination pattern. The tricky part is draining in-flight work during shutdown: close the input channel first, then wait on a WaitGroup..." } } ``` With `"model": "claude-opus-5"`, the advice is encrypted: ```json { "type": "advisor_tool_result", "tool_use_id": "srvtoolu_abc123", "content": { "type": "advisor_redacted_result", "encrypted_content": "EqQBCkYIBRgCIiQ5ZjE0N2M2OC0yYWIxLTRkZTktYjA3ZC1hZTUyMzkxYjhkMmU..." } } ``` Both result variants carry a `stop_reason` field when you set [`max_tokens`](https://platform.claude.com/docs/en/agents-and-tools/tool-use/advisor-tool#capping-advisor-output) on the tool definition, and omit it when you do not. It holds the advisor sub-call's stop reason, typically `"end_turn"`, or `"max_tokens"` when the cap is hit. The values match the top-level Messages API [`stop_reason`](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons). With `advisor_result`, the `text` field contains human-readable advice. With `advisor_redacted_result`, the `encrypted_content` field contains an opaque blob that you cannot read. On the next turn, the server decrypts it and renders the plaintext into the executor's prompt. In both cases, round-trip the content verbatim on subsequent turns. If you switch advisor models mid-conversation, branch on `content.type` to handle both shapes. ### Error results If the advisor call fails, the result carries an error: ```json { "type": "advisor_tool_result", "tool_use_id": "srvtoolu_abc123", "content": { "type": "advisor_tool_result_error", "error_code": "overloaded" } } ``` The executor sees the error and continues without further advice. The request itself does not fail. | `error_code` | Meaning | | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | `max_uses_exceeded` | The request reached the `max_uses` cap set on the tool definition. Further advisor calls in the same request return this error. | | `too_many_requests` | The advisor sub-inference was rate-limited. | | `overloaded` | The advisor sub-inference hit capacity limits. | | `prompt_too_long` | The transcript exceeded the advisor model's context window. | | `execution_time_exceeded` | The advisor sub-inference timed out. | | `model_not_found` | The configured advisor model is not available. | | `unavailable` | Any other advisor failure. | Advisor rate limits draw from the same per-model bucket as direct calls to the advisor model. A rate limit on the advisor appears as `too_many_requests` inside the tool result. A rate limit on the executor fails the whole request with HTTP 429. ## Multi-turn conversations Pass the full assistant content, including `advisor_tool_result` blocks, back to the API on subsequent turns. Round-trip the result blocks verbatim: with a Claude Opus 5 advisor the result block's `content` is the encrypted `advisor_redacted_result` variant, and the server decrypts it and renders the advice into the executor's prompt on the next turn (see [Result variants](https://platform.claude.com/docs/en/agents-and-tools/tool-use/advisor-tool#result-variants)). The mechanics are identical for any advisor model. ```python Python client = anthropic.Anthropic() tools = [ { "type": "advisor_20260301", "name": "advisor", "model": "claude-opus-5", } ] messages = [ { "role": "user", "content": "Build a concurrent worker pool in Go with graceful shutdown.", } ] response = client.beta.messages.create( model="claude-sonnet-5", max_tokens=1024, betas=["advisor-tool-2026-03-01"], tools=tools, messages=messages, ) # Append the full response content, including any advisor_tool_result blocks messages.append({"role": "assistant", "content": response.content}) # Continue the conversation messages.append({"role": "user", "content": "Now add a max-in-flight limit of 10."}) response = client.beta.messages.create( model="claude-sonnet-5", max_tokens=1024, betas=["advisor-tool-2026-03-01"], tools=tools, messages=messages, ) ``` ```typescript TypeScript const client = new Anthropic(); const tools: Anthropic.Beta.Messages.BetaToolUnion[] = [ { type: "advisor_20260301", name: "advisor", model: "claude-opus-5" } ]; const messages: Anthropic.Beta.Messages.BetaMessageParam[] = [ { role: "user", content: "Build a concurrent worker pool in Go with graceful shutdown." } ]; const response = await client.beta.messages.create({ model: "claude-sonnet-5", max_tokens: 1024, betas: ["advisor-tool-2026-03-01"], tools, messages }); // Append the full response content, including any advisor_tool_result blocks messages.push({ role: "assistant", content: response.content }); // Continue the conversation messages.push({ role: "user", content: "Now add a max-in-flight limit of 10." }); const followUp = await client.beta.messages.create({ model: "claude-sonnet-5", max_tokens: 1024, betas: ["advisor-tool-2026-03-01"], tools, messages }); ``` ```csharp C# using Anthropic.Models.Beta.Messages; using Messages = Anthropic.Models.Messages; var client = new AnthropicClient(); var tools = new BetaToolUnion[] { new BetaAdvisorTool20260301 { Model = Messages::Model.ClaudeOpus5 } }; var messages = new List { new() { Role = Role.User, Content = "Build a concurrent worker pool in Go with graceful shutdown." } }; var response = await client.Beta.Messages.Create(new MessageCreateParams { Model = Messages::Model.ClaudeSonnet5, MaxTokens = 1024, Tools = tools, Messages = messages, Betas = ["advisor-tool-2026-03-01"] }); // Append the full response content, including any advisor_tool_result blocks messages.Add(new BetaMessageParam { Role = Role.Assistant, Content = response.Content.Select(block => new BetaContentBlockParam(block.Json)).ToList() }); // Continue the conversation messages.Add(new BetaMessageParam { Role = Role.User, Content = "Now add a max-in-flight limit of 10." }); var followUp = await client.Beta.Messages.Create(new MessageCreateParams { Model = Messages::Model.ClaudeSonnet5, MaxTokens = 1024, Tools = tools, Messages = messages, Betas = ["advisor-tool-2026-03-01"] }); ``` ```go Go client := anthropic.NewClient() tools := []anthropic.BetaToolUnionParam{ {OfAdvisorTool20260301: &anthropic.BetaAdvisorTool20260301Param{ Model: anthropic.ModelClaudeOpus5, }}, } messages := []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Build a concurrent worker pool in Go with graceful shutdown.")), } response, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeSonnet5, MaxTokens: 1024, Tools: tools, Messages: messages, Betas: []anthropic.AnthropicBeta{ anthropic.AnthropicBetaAdvisorTool2026_03_01, }, }) if err != nil { log.Fatal(err) } // Append the full response content, including any advisor_tool_result blocks. // BetaMessage.ToParam drops advisor result content as of anthropic-sdk-go // v1.61.0, so re-parse each response block's raw JSON into a param block instead. assistantContent := make([]anthropic.BetaContentBlockParamUnion, len(response.Content)) for i, block := range response.Content { if err := json.Unmarshal([]byte(block.RawJSON()), &assistantContent[i]); err != nil { log.Fatal(err) } } messages = append(messages, anthropic.BetaMessageParam{ Role: anthropic.BetaMessageParamRoleAssistant, Content: assistantContent, }) // Continue the conversation messages = append(messages, anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Now add a max-in-flight limit of 10."))) response, err = client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeSonnet5, MaxTokens: 1024, Tools: tools, Messages: messages, Betas: []anthropic.AnthropicBeta{ anthropic.AnthropicBetaAdvisorTool2026_03_01, }, }) if err != nil { log.Fatal(err) } ``` ```java Java import com.anthropic.models.beta.messages.BetaAdvisorTool20260301; import com.anthropic.models.beta.messages.BetaContentBlock; import com.anthropic.models.beta.messages.BetaMessage; import com.anthropic.models.beta.messages.BetaMessageParam; import com.anthropic.models.beta.messages.BetaToolUnion; import com.anthropic.models.beta.messages.MessageCreateParams; import com.anthropic.models.messages.Model; void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); List tools = List.of( BetaToolUnion.ofAdvisorTool20260301( BetaAdvisorTool20260301.builder().model(Model.CLAUDE_OPUS_5).build())); List messages = new ArrayList<>(); messages.add(BetaMessageParam.builder() .role(BetaMessageParam.Role.USER) .content("Build a concurrent worker pool in Go with graceful shutdown.") .build()); BetaMessage response = client.beta().messages().create(MessageCreateParams.builder() .model(Model.CLAUDE_SONNET_5) .maxTokens(1024L) .tools(tools) .messages(messages) .addBeta("advisor-tool-2026-03-01") .build()); // Append the full response content, including any advisor_tool_result blocks messages.add(BetaMessageParam.builder() .role(BetaMessageParam.Role.ASSISTANT) .contentOfBetaContentBlockParams( response.content().stream().map(BetaContentBlock::toParam).toList()) .build()); // Continue the conversation messages.add(BetaMessageParam.builder() .role(BetaMessageParam.Role.USER) .content("Now add a max-in-flight limit of 10.") .build()); BetaMessage followUp = client.beta().messages().create(MessageCreateParams.builder() .model(Model.CLAUDE_SONNET_5) .maxTokens(1024L) .tools(tools) .messages(messages) .addBeta("advisor-tool-2026-03-01") .build()); } ``` ```php PHP $client = new Client(); $tools = [ [ 'type' => 'advisor_20260301', 'name' => 'advisor', 'model' => 'claude-opus-5', ], ]; $messages = [ [ 'role' => 'user', 'content' => 'Build a concurrent worker pool in Go with graceful shutdown.', ], ]; $response = $client->beta->messages->create( maxTokens: 1024, messages: $messages, model: 'claude-sonnet-5', tools: $tools, betas: ['advisor-tool-2026-03-01'], ); // Append the full response content, including any advisor_tool_result blocks $messages[] = ['role' => 'assistant', 'content' => $response->content]; // Continue the conversation $messages[] = ['role' => 'user', 'content' => 'Now add a max-in-flight limit of 10.']; $response = $client->beta->messages->create( maxTokens: 1024, messages: $messages, model: 'claude-sonnet-5', tools: $tools, betas: ['advisor-tool-2026-03-01'], ); ``` ```ruby Ruby client = Anthropic::Client.new tools = [ { type: "advisor_20260301", name: "advisor", model: "claude-opus-5" } ] messages = [ { role: "user", content: "Build a concurrent worker pool in Go with graceful shutdown." } ] response = client.beta.messages.create( model: "claude-sonnet-5", max_tokens: 1024, tools: tools, messages: messages, betas: ["advisor-tool-2026-03-01"] ) # Append the full response content, including any advisor_tool_result blocks messages << { role: "assistant", content: response.content } # Continue the conversation messages << { role: "user", content: "Now add a max-in-flight limit of 10." } response = client.beta.messages.create( model: "claude-sonnet-5", max_tokens: 1024, tools: tools, messages: messages, betas: ["advisor-tool-2026-03-01"] ) ``` You can drop the advisor tool from `tools` on a follow-up turn while the message history still contains `advisor_tool_result` blocks. The request is accepted and the historical blocks are preserved; the model cannot call the advisor on that turn. You must still send the `advisor-tool-2026-03-01` beta header for those history blocks to be accepted. The advisor tool has no built-in conversation-level cap. To limit advisor calls across a conversation, count them client-side. When you reach your ceiling, remove the advisor tool from your `tools` array. You do not need to strip `advisor_tool_result` blocks from your message history. ### Resuming a paused turn A response can end with `stop_reason: "pause_turn"` while an advisor call is still pending. When that occurs, the response contains the advisor's `server_tool_use` block with no `advisor_tool_result` for it. To resume, append that assistant message to `messages` with its content unchanged, keeping the `server_tool_use` block, and send the request again with the same advisor tool and beta header. You do not need to add a user message or a `tool_result` block. The API runs the pending advisor call and continues the executor's turn in the new response. A resumed turn can pause again. If it does, repeat the same step. Omitting the advisor tool from the resume request returns a 400 `invalid_request_error`, because the pending `server_tool_use` block has no tool definition to run against; include the tool whenever a call is pending. If instead the executor called one of your tools in the same turn, the response ends with `stop_reason: "tool_use"` while the advisor call is still pending. Send the `tool_result` blocks as usual, and the pending advisor call runs at the start of that next request. See [Mixing server tools and client tools in one turn](https://platform.claude.com/docs/en/agents-and-tools/tool-use/server-tools#mixing-server-tools-and-client-tools-in-one-turn). ### Mid-conversation nudge for under-calling executors If a Haiku executor has not called the advisor in its first assistant turn, append a short reminder as an additional user message before the second assistant turn. In Anthropic's internal behavioral evaluation this raised task pass rates by roughly 7 percentage points on Haiku executors. On Sonnet executors, the plain-text nudge had no measurable effect in Anthropic's testing. The call-timing considerations that follow are especially relevant for Sonnet. Do not apply the nudge to Opus executors: On Opus it slightly lowered pass rates. With the default `NUDGE_TURN` of 2, the reminder typically arrives after the model has oriented on the task but before it has committed to an approach. ```python Python client = anthropic.Anthropic() NUDGE_TURN = 2 # inject before this assistant turn if no advisor call yet NUDGE_TEXT = ( "You have not consulted the advisor yet. If the task has a non-obvious " "design decision or a failure mode you haven't ruled out, call advisor " "now before committing to an approach." ) MAX_TURNS = 10 # agent loop cap def run_your_tools(content): # Replace with your tool dispatch. Returns one tool_result block per tool_use block. return [ { "type": "tool_result", "tool_use_id": block.id, "content": "Replace with your tool output.", } for block in content if block.type == "tool_use" ] tools = [ {"type": "advisor_20260301", "name": "advisor", "model": "claude-opus-5"}, # ... your other tools ] task = "Build a concurrent worker pool in Go with graceful shutdown." messages = [{"role": "user", "content": task}] advisor_called = False for turn in range(1, MAX_TURNS + 1): response = client.beta.messages.create( model="claude-haiku-4-5", max_tokens=4096, betas=["advisor-tool-2026-03-01"], tools=tools, messages=messages, ) messages.append({"role": "assistant", "content": response.content}) advisor_called = advisor_called or any( block.type == "server_tool_use" and block.name == "advisor" for block in response.content ) if response.stop_reason == "end_turn": break if response.stop_reason == "pause_turn": continue # server tool pending; re-send to let the API complete it results = run_your_tools(response.content) # list of tool_result blocks if results: messages.append({"role": "user", "content": results}) # Skip this if your system prompt already tells the model to call sparingly. if turn == NUDGE_TURN - 1 and not advisor_called: messages.append({"role": "user", "content": NUDGE_TEXT}) ``` ```typescript TypeScript const client = new Anthropic(); const NUDGE_TURN = 2; // inject before this assistant turn if no advisor call yet const NUDGE_TEXT = "You have not consulted the advisor yet. If the task has a non-obvious " + "design decision or a failure mode you haven't ruled out, call advisor " + "now before committing to an approach."; const MAX_TURNS = 10; // agent loop cap function runYourTools( content: Anthropic.Beta.Messages.BetaContentBlock[] ): Anthropic.Beta.Messages.BetaToolResultBlockParam[] { // Replace with your tool dispatch. Returns one tool_result block per tool_use block. return content .filter((block) => block.type === "tool_use") .map((block) => ({ type: "tool_result" as const, tool_use_id: block.id, content: "Replace with your tool output." })); } const tools: Anthropic.Beta.Messages.BetaToolUnion[] = [ { type: "advisor_20260301", name: "advisor", model: "claude-opus-5" } // ... your other tools ]; const task = "Build a concurrent worker pool in Go with graceful shutdown."; const messages: Anthropic.Beta.Messages.BetaMessageParam[] = [{ role: "user", content: task }]; let advisorCalled = false; for (let turn = 1; turn <= MAX_TURNS; turn++) { const response = await client.beta.messages.create({ model: "claude-haiku-4-5", max_tokens: 4096, betas: ["advisor-tool-2026-03-01"], tools, messages }); messages.push({ role: "assistant", content: response.content }); advisorCalled = advisorCalled || response.content.some( (block) => block.type === "server_tool_use" && block.name === "advisor" ); if (response.stop_reason === "end_turn") { break; } if (response.stop_reason === "pause_turn") { continue; // server tool pending; re-send to let the API complete it } const results = runYourTools(response.content); // list of tool_result blocks if (results.length > 0) { messages.push({ role: "user", content: results }); } // Skip this if your system prompt already tells the model to call sparingly. if (turn === NUDGE_TURN - 1 && !advisorCalled) { messages.push({ role: "user", content: NUDGE_TEXT }); } } ``` ```csharp C# using Anthropic.Models.Beta.Messages; using Messages = Anthropic.Models.Messages; var client = new AnthropicClient(); const int NudgeTurn = 2; // inject before this assistant turn if no advisor call yet const string NudgeText = "You have not consulted the advisor yet. If the task has a non-obvious " + "design decision or a failure mode you haven't ruled out, call advisor " + "now before committing to an approach."; const int MaxTurns = 10; // agent loop cap // Replace with your tool dispatch. Returns one tool_result block per tool_use block. List RunYourTools(IReadOnlyList content) { List results = []; foreach (var block in content) { if (block.TryPickToolUse(out var toolUse)) { results.Add(new BetaToolResultBlockParam { ToolUseID = toolUse.ID, Content = "Replace with your tool output." }); } } return results; } var tools = new BetaToolUnion[] { new BetaAdvisorTool20260301 { Model = Messages::Model.ClaudeOpus5 } // ... your other tools }; var task = "Build a concurrent worker pool in Go with graceful shutdown."; var messages = new List { new() { Role = Role.User, Content = task } }; var advisorCalled = false; for (var turn = 1; turn <= MaxTurns; turn++) { var response = await client.Beta.Messages.Create(new MessageCreateParams { Model = Messages::Model.ClaudeHaiku4_5, MaxTokens = 4096, Tools = tools, Messages = messages, Betas = ["advisor-tool-2026-03-01"] }); messages.Add(new BetaMessageParam { Role = Role.Assistant, Content = response.Content.Select(block => new BetaContentBlockParam(block.Json)).ToList() }); advisorCalled = advisorCalled || response.Content.Any(block => block.TryPickServerToolUse(out var serverToolUse) && serverToolUse.Name.Value() == Name.Advisor ); if (response.StopReason == BetaStopReason.EndTurn) { break; } if (response.StopReason == BetaStopReason.PauseTurn) { continue; // server tool pending; re-send to let the API complete it } var results = RunYourTools(response.Content); // list of tool_result blocks if (results.Count > 0) { messages.Add(new BetaMessageParam { Role = Role.User, Content = results }); } // Skip this if your system prompt already tells the model to call sparingly. if (turn == NudgeTurn - 1 && !advisorCalled) { messages.Add(new BetaMessageParam { Role = Role.User, Content = NudgeText }); } } ``` ```go Go const ( nudgeTurn = 2 // inject before this assistant turn if no advisor call yet nudgeText = "You have not consulted the advisor yet. If the task has a non-obvious " + "design decision or a failure mode you haven't ruled out, call advisor " + "now before committing to an approach." maxTurns = 10 // agent loop cap ) // Replace with your tool dispatch. Returns one tool_result block per tool_use block. func runYourTools(content []anthropic.BetaContentBlockUnion) []anthropic.BetaContentBlockParamUnion { var results []anthropic.BetaContentBlockParamUnion for _, block := range content { if block.Type == "tool_use" { results = append(results, anthropic.NewBetaToolResultBlock(block.ID, "Replace with your tool output.", false)) } } return results } func main() { client := anthropic.NewClient() tools := []anthropic.BetaToolUnionParam{ {OfAdvisorTool20260301: &anthropic.BetaAdvisorTool20260301Param{ Model: anthropic.ModelClaudeOpus5, }}, // ... your other tools } task := "Build a concurrent worker pool in Go with graceful shutdown." messages := []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock(task)), } advisorCalled := false for turn := 1; turn <= maxTurns; turn++ { response, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeHaiku4_5, MaxTokens: 4096, Tools: tools, Messages: messages, Betas: []anthropic.AnthropicBeta{ anthropic.AnthropicBetaAdvisorTool2026_03_01, }, }) if err != nil { log.Fatal(err) } // Append the full response content, including any advisor_tool_result blocks. // BetaMessage.ToParam drops advisor result content as of anthropic-sdk-go // v1.61.0, so re-parse each response block's raw JSON into a param block instead. assistantContent := make([]anthropic.BetaContentBlockParamUnion, len(response.Content)) for i, block := range response.Content { if err := json.Unmarshal([]byte(block.RawJSON()), &assistantContent[i]); err != nil { log.Fatal(err) } } messages = append(messages, anthropic.BetaMessageParam{ Role: anthropic.BetaMessageParamRoleAssistant, Content: assistantContent, }) for _, block := range response.Content { if block.Type == "server_tool_use" && block.Name == "advisor" { advisorCalled = true } } if response.StopReason == anthropic.BetaStopReasonEndTurn { break } if response.StopReason == anthropic.BetaStopReasonPauseTurn { continue // server tool pending; re-send to let the API complete it } results := runYourTools(response.Content) // list of tool_result blocks if len(results) > 0 { messages = append(messages, anthropic.BetaMessageParam{ Role: anthropic.BetaMessageParamRoleUser, Content: results, }) } // Skip this if your system prompt already tells the model to call sparingly. if turn == nudgeTurn-1 && !advisorCalled { messages = append(messages, anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock(nudgeText))) } } } ``` ```java Java import com.anthropic.models.beta.messages.BetaAdvisorTool20260301; import com.anthropic.models.beta.messages.BetaContentBlock; import com.anthropic.models.beta.messages.BetaContentBlockParam; import com.anthropic.models.beta.messages.BetaMessage; import com.anthropic.models.beta.messages.BetaMessageParam; import com.anthropic.models.beta.messages.BetaServerToolUseBlock; import com.anthropic.models.beta.messages.BetaStopReason; import com.anthropic.models.beta.messages.BetaToolResultBlockParam; import com.anthropic.models.beta.messages.BetaToolUnion; import com.anthropic.models.beta.messages.MessageCreateParams; import com.anthropic.models.messages.Model; static final int NUDGE_TURN = 2; // inject before this assistant turn if no advisor call yet static final String NUDGE_TEXT = "You have not consulted the advisor yet. If the task has a non-obvious " + "design decision or a failure mode you haven't ruled out, call advisor " + "now before committing to an approach."; static final int MAX_TURNS = 10; // agent loop cap // Replace with your tool dispatch. Returns one tool_result block per tool_use block. List runYourTools(List content) { List results = new ArrayList<>(); for (BetaContentBlock block : content) { if (block.isToolUse()) { results.add(BetaContentBlockParam.ofToolResult( BetaToolResultBlockParam.builder() .toolUseId(block.asToolUse().id()) .content("Replace with your tool output.") .build())); } } return results; } void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); List tools = List.of( BetaToolUnion.ofAdvisorTool20260301( BetaAdvisorTool20260301.builder().model(Model.CLAUDE_OPUS_5).build()) // ... your other tools ); String task = "Build a concurrent worker pool in Go with graceful shutdown."; List messages = new ArrayList<>(); messages.add(BetaMessageParam.builder() .role(BetaMessageParam.Role.USER) .content(task) .build()); boolean advisorCalled = false; for (int turn = 1; turn <= MAX_TURNS; turn++) { BetaMessage response = client.beta().messages().create(MessageCreateParams.builder() .model(Model.CLAUDE_HAIKU_4_5) .maxTokens(4096L) .tools(tools) .messages(messages) .addBeta("advisor-tool-2026-03-01") .build()); messages.add(BetaMessageParam.builder() .role(BetaMessageParam.Role.ASSISTANT) .contentOfBetaContentBlockParams( response.content().stream().map(BetaContentBlock::toParam).toList()) .build()); advisorCalled = advisorCalled || response.content().stream().anyMatch(block -> block.isServerToolUse() && block.asServerToolUse().name().equals(BetaServerToolUseBlock.Name.ADVISOR)); BetaStopReason stopReason = response.stopReason().orElse(null); if (BetaStopReason.END_TURN.equals(stopReason)) { break; } if (BetaStopReason.PAUSE_TURN.equals(stopReason)) { continue; // server tool pending; re-send to let the API complete it } List results = runYourTools(response.content()); // list of tool_result blocks if (!results.isEmpty()) { messages.add(BetaMessageParam.builder() .role(BetaMessageParam.Role.USER) .contentOfBetaContentBlockParams(results) .build()); } // Skip this if your system prompt already tells the model to call sparingly. if (turn == NUDGE_TURN - 1 && !advisorCalled) { messages.add(BetaMessageParam.builder() .role(BetaMessageParam.Role.USER) .content(NUDGE_TEXT) .build()); } } } ``` ```php PHP $client = new Client(); const NUDGE_TURN = 2; // inject before this assistant turn if no advisor call yet const NUDGE_TEXT = "You have not consulted the advisor yet. If the task has a non-obvious " . "design decision or a failure mode you haven't ruled out, call advisor " . "now before committing to an approach."; const MAX_TURNS = 10; // agent loop cap // Replace with your tool dispatch. Returns one tool_result block per tool_use block. function runYourTools(array $content): array { $results = []; foreach ($content as $block) { if ($block->type === 'tool_use') { $results[] = [ 'type' => 'tool_result', 'tool_use_id' => $block->id, 'content' => 'Replace with your tool output.', ]; } } return $results; } $tools = [ ['type' => 'advisor_20260301', 'name' => 'advisor', 'model' => 'claude-opus-5'], // ... your other tools ]; $task = 'Build a concurrent worker pool in Go with graceful shutdown.'; $messages = [['role' => 'user', 'content' => $task]]; $advisorCalled = false; for ($turn = 1; $turn <= MAX_TURNS; $turn++) { $response = $client->beta->messages->create( maxTokens: 4096, messages: $messages, model: 'claude-haiku-4-5', tools: $tools, betas: ['advisor-tool-2026-03-01'], ); $messages[] = ['role' => 'assistant', 'content' => $response->content]; foreach ($response->content as $block) { if ($block->type === 'server_tool_use' && $block->name === 'advisor') { $advisorCalled = true; } } if ($response->stopReason === 'end_turn') { break; } if ($response->stopReason === 'pause_turn') { continue; // server tool pending; re-send to let the API complete it } $results = runYourTools($response->content); // list of tool_result blocks if ($results !== []) { $messages[] = ['role' => 'user', 'content' => $results]; } // Skip this if your system prompt already tells the model to call sparingly. if ($turn === NUDGE_TURN - 1 && !$advisorCalled) { $messages[] = ['role' => 'user', 'content' => NUDGE_TEXT]; } } ``` ```ruby Ruby client = Anthropic::Client.new NUDGE_TURN = 2 # inject before this assistant turn if no advisor call yet NUDGE_TEXT = "You have not consulted the advisor yet. If the task has a non-obvious " \ "design decision or a failure mode you haven't ruled out, call advisor " \ "now before committing to an approach." MAX_TURNS = 10 # agent loop cap # Replace with your tool dispatch. Returns one tool_result block per tool_use block. def run_your_tools(content) content.filter_map do |block| next unless block.type == :tool_use { type: "tool_result", tool_use_id: block.id, content: "Replace with your tool output." } end end tools = [ { type: "advisor_20260301", name: "advisor", model: "claude-opus-5" } # ... your other tools ] task = "Build a concurrent worker pool in Go with graceful shutdown." messages = [{ role: "user", content: task }] advisor_called = false (1..MAX_TURNS).each do |turn| response = client.beta.messages.create( model: "claude-haiku-4-5", max_tokens: 4096, tools: tools, messages: messages, betas: ["advisor-tool-2026-03-01"] ) messages << { role: "assistant", content: response.content } advisor_called ||= response.content.any? do |block| block.type == :server_tool_use && block.name == :advisor end break if response.stop_reason == :end_turn next if response.stop_reason == :pause_turn # server tool pending; re-send to let the API complete it results = run_your_tools(response.content) # list of tool_result blocks messages << { role: "user", content: results } unless results.empty? # Skip this if your system prompt already tells the model to call sparingly. messages << { role: "user", content: NUDGE_TEXT } if turn == NUDGE_TURN - 1 && !advisor_called end ``` Append the nudge as its own user message after the tool results rather than as a sibling block in the same message. Consecutive user messages are valid. In Anthropic's testing on Haiku and Sonnet executors they behaved equivalently to a sibling block. The separate-message shape also keeps the reminder clearly distinct from tool output. **Trade-offs:** The nudge raises the call rate, which can push trivially simple tasks into an unnecessary consult. If your workload mixes simple and complex tasks, consider raising `NUDGE_TURN` to 3 so two-turn tasks complete before the nudge fires, or gate the nudge on a task-complexity signal you already compute. If your system prompt already contains restraint language ("reserve the advisor for genuine uncertainty"), skip the nudge entirely, because the two instructions conflict. The plain-text nudge is highly salient on Haiku and Sonnet executors: 74 percent (Sonnet) to 98 percent (Haiku) of nudged attempts in Anthropic's testing called the advisor immediately at turn 2. If that lands before your executor has read the problem or gathered context, the resulting advisor call is low-context and can displace a better-timed later call. Measure your executor's baseline first-call turn before adding the nudge. If the executor already calls the advisor reliably and its first call typically lands at turn N, set `NUDGE_TURN` greater than N. In Anthropic's testing, a turn-2 nudge on workloads where the baseline first call was turn 7 or later correlated with a 3 to 4 percentage-point task-performance drop. On a browse workload where the baseline call rate was 86 percent, the same nudge raised engagement with no task-performance cost. To force a consult on a specific request instead of nudging, set `tool_choice` to `{"type": "tool", "name": "advisor"}`, subject to the constraints in [Forcing tool use](https://platform.claude.com/docs/en/agents-and-tools/tool-use/define-tools#forcing-tool-use). Forcing tool use cannot be combined with manual extended thinking (`thinking: {type: "enabled"}`): the API returns a `400 invalid_request_error` if you enable both. Adaptive thinking supports forced tool use. ## Streaming The advisor sub-inference does not stream. The executor's stream pauses while the advisor runs; then the full result arrives in a single event. The `server_tool_use` block with `name: "advisor"` signals that an advisor call is starting. The pause begins when that block closes (`content_block_stop`). During the pause, the stream is quiet except for standard SSE `ping` keepalives emitted roughly every 30 seconds. Short advisor calls might show no pings. When the advisor finishes, the `advisor_tool_result` arrives fully formed in a single `content_block_start` event (no deltas). Executor output then resumes streaming. A `message_delta` event follows with the updated `usage.iterations` array reflecting the advisor's token counts. ## Usage and billing Advisor calls run as a separate sub-inference billed at the advisor model's rates. Usage is reported in the `usage.iterations[]` array: ```json { "usage": { "input_tokens": 1760, "cache_read_input_tokens": 412, "cache_creation_input_tokens": 0, "output_tokens": 531, "iterations": [ { "type": "message", "input_tokens": 412, "cache_read_input_tokens": 0, "cache_creation_input_tokens": 0, "output_tokens": 89 }, { "type": "advisor_message", "model": "claude-opus-5", "input_tokens": 823, "cache_read_input_tokens": 0, "cache_creation_input_tokens": 0, "output_tokens": 1612 }, { "type": "message", "input_tokens": 1348, "cache_read_input_tokens": 412, "cache_creation_input_tokens": 0, "output_tokens": 442 } ] } } ``` Top-level `usage` fields reflect executor tokens only. Advisor tokens are not rolled into the top-level totals because they are billed at a different rate. Iterations with `type: "advisor_message"` are billed at the advisor model's rates, and iterations with `type: "message"` are billed at the executor model's rates. Every top-level `usage` field is the sum of that field across all executor iterations, including `input_tokens`, `output_tokens`, and `cache_read_input_tokens`. Because each executor iteration re-sends the growing conversation, later iterations' inputs include earlier iterations' output, so summed `input_tokens` exceeds the size of any single prompt. Use `usage.iterations` for a full per-iteration breakdown when building cost-tracking logic. Advisor output is typically 400 to 700 text tokens, or 1,400 to 1,800 tokens total including thinking. The cost savings come from the advisor not generating your full final output. The executor does that at its lower rate. The top-level `max_tokens` applies to executor output only. It does not bound advisor sub-inference tokens. To cap advisor output directly, set [`max_tokens` on the tool definition](https://platform.claude.com/docs/en/agents-and-tools/tool-use/advisor-tool#capping-advisor-output). The advisor's tokens also do not draw from any [task budget](https://platform.claude.com/docs/en/build-with-claude/task-budgets) applied to the executor. [Priority Tier](https://platform.claude.com/docs/en/api/service-tiers) applies to each model independently. A Priority Tier commitment on the executor model does not extend to the advisor. Advisor calls run at Priority Tier only if your organization also holds a commitment on the advisor model. ## Advisor prompt caching There are two independent caching layers. ### Executor-side caching The `advisor_tool_result` block is cacheable like any other content block. A `cache_control` breakpoint placed after it on a subsequent turn hits. The executor's prompt always contains the plaintext advice regardless of whether your client received `text` or `encrypted_content`, so caching behavior is identical for both result variants. ### Advisor-side caching Set `caching` on the tool definition to enable prompt caching for the advisor's own transcript across calls within the same conversation: ```python Python tools = [ { "type": "advisor_20260301", "name": "advisor", "model": "claude-opus-5", "caching": {"type": "ephemeral", "ttl": "5m"}, } ] ``` ```typescript TypeScript const tools: Anthropic.Beta.Messages.BetaToolUnion[] = [ { type: "advisor_20260301", name: "advisor", model: "claude-opus-5", caching: { type: "ephemeral", ttl: "5m" } } ]; ``` ```csharp C# using Anthropic.Models.Beta.Messages; using Messages = Anthropic.Models.Messages; var tools = new BetaToolUnion[] { new BetaAdvisorTool20260301 { Model = Messages::Model.ClaudeOpus5, Caching = new BetaCacheControlEphemeral { Ttl = Ttl.Ttl5m } } }; ``` ```go Go tools := []anthropic.BetaToolUnionParam{ {OfAdvisorTool20260301: &anthropic.BetaAdvisorTool20260301Param{ Model: anthropic.ModelClaudeOpus5, Caching: anthropic.BetaCacheControlEphemeralParam{TTL: anthropic.BetaCacheControlEphemeralTTLTTL5m}, }}, } ``` ```java Java import com.anthropic.models.beta.messages.BetaAdvisorTool20260301; import com.anthropic.models.beta.messages.BetaCacheControlEphemeral; import com.anthropic.models.beta.messages.BetaToolUnion; import com.anthropic.models.messages.Model; List tools = List.of( BetaToolUnion.ofAdvisorTool20260301(BetaAdvisorTool20260301.builder() .model(Model.CLAUDE_OPUS_5) .caching(BetaCacheControlEphemeral.builder() .ttl(BetaCacheControlEphemeral.Ttl.TTL_5M) .build()) .build())); ``` ```php PHP $tools = [ [ 'type' => 'advisor_20260301', 'name' => 'advisor', 'model' => 'claude-opus-5', 'caching' => ['type' => 'ephemeral', 'ttl' => '5m'], ], ]; ``` ```ruby Ruby tools = [ { type: "advisor_20260301", name: "advisor", model: "claude-opus-5", caching: { type: "ephemeral", ttl: "5m" } } ] ``` The advisor's prompt on the Nth call is the (N-1)th call's prompt with one more segment appended, so the prefix is stable across calls. With `caching` enabled, each advisor call writes a cache entry, and the next call reads up to that point and pays only for the delta. You'll see `cache_read_input_tokens` become non-zero on the second and later `advisor_message` iterations. **When to enable it:** The cache write costs more than the reads save when the advisor is called two or fewer times per conversation. Caching breaks even at roughly three advisor calls and improves from there. Enable it for long agent loops, and keep it off for short tasks. **Keep it consistent:** Set `caching` once and leave it for the whole conversation. Toggling it off and on mid-conversation causes cache misses. [`clear_thinking`](https://platform.claude.com/docs/en/build-with-claude/context-editing) with a `keep` value other than `"all"` shifts the advisor's quoted transcript each turn, causing advisor-side cache misses. This is a cost degradation only. Advice quality is unaffected. When extended thinking is enabled without explicit `clear_thinking` configuration, the API defaults to `keep: {type: "thinking_turns", value: 1}`, which triggers this behavior (the default on earlier Opus/Sonnet models and all Haiku models, whereas on Opus 4.5+ and Sonnet 4.6+ the default is to keep all turns). Set `keep: "all"` to preserve advisor cache stability. ## Combining with other tools The advisor tool composes with other server-side and client-side tools. Add them all to the same `tools` array: ```python Python tools = [ { "type": "web_search_20250305", "name": "web_search", "max_uses": 5, }, { "type": "advisor_20260301", "name": "advisor", "model": "claude-opus-5", }, { "name": "run_bash", "description": "Run a bash command", "input_schema": { "type": "object", "properties": {"command": {"type": "string"}}, }, }, ] ``` ```typescript TypeScript const tools: Anthropic.Beta.Messages.BetaToolUnion[] = [ { type: "web_search_20250305", name: "web_search", max_uses: 5 }, { type: "advisor_20260301", name: "advisor", model: "claude-opus-5" }, { name: "run_bash", description: "Run a bash command", input_schema: { type: "object", properties: { command: { type: "string" } } } } ]; ``` ```csharp C# using System.Text.Json; using Anthropic.Models.Beta.Messages; using Messages = Anthropic.Models.Messages; var tools = new BetaToolUnion[] { new BetaWebSearchTool20250305 { MaxUses = 5 }, new BetaAdvisorTool20260301 { Model = Messages::Model.ClaudeOpus5 }, new BetaTool { Name = "run_bash", Description = "Run a bash command", InputSchema = new() { Properties = new Dictionary { ["command"] = JsonSerializer.SerializeToElement(new { type = "string" }) } } } }; ``` ```go Go tools := []anthropic.BetaToolUnionParam{ {OfWebSearchTool20250305: &anthropic.BetaWebSearchTool20250305Param{ MaxUses: anthropic.Int(5), }}, {OfAdvisorTool20260301: &anthropic.BetaAdvisorTool20260301Param{ Model: anthropic.ModelClaudeOpus5, }}, {OfTool: &anthropic.BetaToolParam{ Name: "run_bash", Description: anthropic.String("Run a bash command"), InputSchema: anthropic.BetaToolInputSchemaParam{ Properties: map[string]any{ "command": map[string]any{"type": "string"}, }, }, }}, } ``` ```java Java import com.anthropic.core.JsonValue; import com.anthropic.models.beta.messages.BetaAdvisorTool20260301; import com.anthropic.models.beta.messages.BetaTool; import com.anthropic.models.beta.messages.BetaToolUnion; import com.anthropic.models.beta.messages.BetaWebSearchTool20250305; import com.anthropic.models.messages.Model; List tools = List.of( BetaToolUnion.ofWebSearchTool20250305(BetaWebSearchTool20250305.builder() .maxUses(5L) .build()), BetaToolUnion.ofAdvisorTool20260301(BetaAdvisorTool20260301.builder() .model(Model.CLAUDE_OPUS_5) .build()), BetaToolUnion.ofBetaTool(BetaTool.builder() .name("run_bash") .description("Run a bash command") .inputSchema(BetaTool.InputSchema.builder() .properties(JsonValue.from(Map.of( "command", Map.of("type", "string")))) .build()) .build())); ``` ```php PHP $tools = [ [ 'type' => 'web_search_20250305', 'name' => 'web_search', 'max_uses' => 5, ], [ 'type' => 'advisor_20260301', 'name' => 'advisor', 'model' => 'claude-opus-5', ], [ 'name' => 'run_bash', 'description' => 'Run a bash command', 'input_schema' => [ 'type' => 'object', 'properties' => ['command' => ['type' => 'string']], ], ], ]; ``` ```ruby Ruby tools = [ { type: "web_search_20250305", name: "web_search", max_uses: 5 }, { type: "advisor_20260301", name: "advisor", model: "claude-opus-5" }, { name: "run_bash", description: "Run a bash command", input_schema: { type: "object", properties: { command: { type: "string" } } } } ] ``` The executor can search the web, call the advisor, and use your custom tools in the same turn. The advisor's plan can inform which tools the executor reaches for next. | Feature | Interaction | | ------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [Batch processing](https://platform.claude.com/docs/en/build-with-claude/batch-processing) | Supported. `usage.iterations` is reported per item. | | [Token counting](https://platform.claude.com/docs/en/build-with-claude/token-counting) | Returns the executor's first-iteration input tokens only. For a rough advisor estimate, call `count_tokens` with `model` set to the advisor model and the same messages. | | [Context editing](https://platform.claude.com/docs/en/build-with-claude/context-editing) | `clear_tool_uses` is not fully compatible with advisor tool blocks. With `clear_thinking`, see the earlier caching warning. | | `pause_turn` | A dangling advisor call ends the response with `stop_reason: "pause_turn"` and a `server_tool_use` block with no result when no client `tool_use` block is awaiting your result in the same turn. The advisor runs on resumption. If the executor also called one of your tools in that turn, the response ends with `stop_reason: "tool_use"` instead, and the pending advisor call runs at the start of your next request, after you send the `tool_result` blocks. See [Resuming a paused turn](https://platform.claude.com/docs/en/agents-and-tools/tool-use/advisor-tool#resuming-a-paused-turn), [Mixing server tools and client tools in one turn](https://platform.claude.com/docs/en/agents-and-tools/tool-use/server-tools#mixing-server-tools-and-client-tools-in-one-turn), and [Server tools](https://platform.claude.com/docs/en/agents-and-tools/tool-use/server-tools#the-server-side-loop-and-pause-turn). | ## Best practices ### Prompting for coding and agent tasks The advisor tool ships with a built-in description that nudges the executor to call it near the start of complex tasks and when it hits difficulty. For research tasks, no additional prompting is typically needed. On coding and agent tasks, the advisor produces higher intelligence at similar cost when it reduces total tool calls and conversation length. Two timings drive this improvement: 1. An early first advisor call, after a few exploratory reads are in the transcript. 2. For difficult tasks, a final advisor call after file writes and test outputs are in the transcript. If your agent exposes other planner-like tools (for example, a todo list tool), prompt the model to call the advisor before those tools so the advisor's plan funnels into them. The [suggested system prompt](https://platform.claude.com/docs/en/agents-and-tools/tool-use/advisor-tool#suggested-system-prompt-for-coding-tasks) reinforces the early-call pattern. Add your own funnel-in sentence pointing at whichever planner tools your agent exposes. #### Suggested system prompt for coding tasks Without system-prompt steering, the executor tends to under-call the advisor in some domains, particularly coding tasks. For coding tasks where you want consistent advisor timing and around two to three calls for each task, prepend the following blocks to your executor system prompt before any other sentences that mention the advisor. Timing guidance: ```text wrap You have access to an `advisor` tool backed by a stronger reviewer model. It takes NO parameters — when you call advisor(), your entire conversation history is automatically forwarded. They see the task, every tool call you've made, every result you've seen. Call advisor BEFORE substantive work — before writing, before committing to an interpretation, before building on an assumption. If the task requires orientation first (finding files, fetching a source, seeing what's there), do that, then call advisor. Orientation is not substantive work. Writing, editing, and declaring an answer are. Also call advisor: - When you believe the task is complete. BEFORE this call, make your deliverable durable: write the file, save the result, commit the change. The advisor call takes time; if the session ends during it, a durable result persists and an unwritten one doesn't. - When stuck — errors recurring, approach not converging, results that don't fit. - When considering a change of approach. On tasks longer than a few steps, call advisor at least once before committing to an approach and once before declaring done. On short reactive tasks where the next action is dictated by tool output you just read, you don't need to keep calling — the advisor adds most of its value on the first call, before the approach crystallizes. ``` How the executor should treat the advice (place directly after the timing block): ```text wrap Give the advice serious weight. If you follow a step and it fails empirically, or you have primary-source evidence that contradicts a specific claim (the file says X, the paper states Y), adapt. A passing self-test is not evidence the advice is wrong — it's evidence your test doesn't check what the advice is checking. If you've already retrieved data pointing one way and the advisor points another: don't silently switch. Surface the conflict in one more advisor call — "I found X, you suggest Y, which constraint breaks the tie?" The advisor saw your evidence but may have underweighted it; a reconcile call is cheaper than committing to the wrong branch. ``` #### Alternative system prompt for Haiku on coding workloads Claude Haiku 4.5 applies the default advisor guidance conservatively. That keeps its call rate appropriately low on research and lookup workloads but gives up quality on coding workloads, where an early advisor consult reliably pays for itself. On an internal coding benchmark, a close variant of the following block (the read-only carve-out in the Hard rule was added after measurement) raised Haiku pass rates by roughly 7.5 percentage points over the built-in default. Use this block in place of the earlier timing and advice blocks when your Haiku executor runs predominantly coding or write-task workloads: ```text wrap Consult a stronger reviewer who sees your full conversation transcript. No parameters. When you call advisor(), your entire history -- task, every tool call and result, your reasoning -- is automatically forwarded. The advisor sees exactly what you've done. Call advisor BEFORE substantive work -- before writing, before committing to an interpretation, before building on an assumption. If the task requires orientation first (finding files, fetching a source, seeing what's there), do that, then call advisor. Orientation is not substantive work. Writing, editing, and declaring an answer are. Also call advisor: - When you believe the task is complete. BEFORE this call, make your deliverable durable: write the file, save the result, commit the change. The advisor call takes time; if the session ends during it, a durable result persists and an unwritten one doesn't. - When stuck -- errors recurring, approach not converging, results that don't fit. - When considering a change of approach. On tasks longer than a few steps, call advisor at least once before committing to an approach and once before declaring done. On short reactive tasks where the next action is dictated by tool output you just read, you don't need to keep calling -- the advisor adds most of its value on the first call, before the approach crystallizes. Give the advice serious weight. If you follow a step and it fails empirically, or you have primary-source evidence that contradicts a specific claim (the file says X, the paper states Y), adapt. A passing self-test is not evidence the advice is wrong -- it's evidence your test doesn't check what the advice is checking. If you've already retrieved data pointing one way and the advisor points another: don't silently switch. Surface the conflict in one more advisor call -- "I found X, you suggest Y, which constraint breaks the tie?" The advisor saw your evidence but may have underweighted it; a reconcile call is cheaper than committing to the wrong branch. Call advisor for design, architecture, and risk questions where you won't touch a file. If your response would be analysis or a recommendation with no other tool calls, call advisor first -- that judgment call is exactly where a second opinion is highest-value. Hard rule: your first write_file, edit_file, or state-changing bash call on a task must be preceded by an advisor call in the same or an earlier turn. Read-only orientation commands (ls, cat, grep, find) are not state-changing. This is a checkpoint, not a difficulty judgment. It applies to one-line edits too. ``` **Caveat:** On an internal browse-comprehension benchmark (n = 1,266), a close variant of this block cost roughly 4 percentage points of accuracy relative to the built-in default. If your workload mixes coding with substantial lookup or retrieval, stay with the [suggested blocks](https://platform.claude.com/docs/en/agents-and-tools/tool-use/advisor-tool#suggested-system-prompt-for-coding-tasks), or gate the swap on a workload-type signal you already compute. #### Increasing advisor calls on Opus executors Opus executors typically call the advisor at an appropriate rate without additional prompting. If your Opus executor is under-calling on your workload, add the following checkpoint to your system prompt: ```text wrap Call advisor for design, architecture, and risk questions where you won't touch a file. If your response would be analysis or a recommendation with no other tool calls, call advisor first. That judgment call is exactly where a second opinion is highest-value. (This does not apply to simple factual lookups or arithmetic; those you answer directly.) Hard rule: your first write_file, edit_file, or state-changing bash call on a task must be preceded by an advisor call in the same or an earlier turn. Read-only orientation commands (ls, cat, grep, find) are not state-changing. This is a checkpoint, not a difficulty judgment. It applies to one-line edits too. ``` **Caveat:** In Anthropic's testing, a close variant of this block (the read-only carve-out in the Hard rule was added after measurement) raised pass rates on under-calling tasks by roughly 7 to 10 percentage points but caused Opus to over-call on tasks whose first action needs no planning. The net effect was roughly flat on a mixed workload. Only add it if you have observed Opus skipping the advisor on tasks where a consult would have helped. Do not add it as a default. #### Trimming advisor output length Advisor output is the advisor's largest cost driver, and the top-level `max_tokens` does not bound it. The advisor sees both your system prompt and your user messages as quoted context about the executor's task, so instructions that address the advisor directly are followed much more reliably than third-person descriptions. The most effective placement Anthropic tested is a line in the user message: ```text wrap (Advisor: please keep your guidance under 80 words — I need a focused starting point, not a comprehensive plan.) ``` This line can be prefixed programmatically by your agent framework before sending the request. The limit is a soft constraint. The advisor occasionally exceeds it, so ask for roughly 80 percent of your true ceiling. In Anthropic's testing this line also increased how often the executor consults the advisor, but the net effect was still lower total cost (more consults, each shorter). Pair this approach with the timing guidance in [Suggested system prompt for coding tasks](https://platform.claude.com/docs/en/agents-and-tools/tool-use/advisor-tool#suggested-system-prompt-for-coding-tasks) (or the [alternative Haiku block](https://platform.claude.com/docs/en/agents-and-tools/tool-use/advisor-tool#alternative-system-prompt-for-haiku-on-coding-workloads) if you swapped it in) for the strongest cost-versus-quality tradeoff. For a hard ceiling rather than a soft request, see [Capping advisor output](https://platform.claude.com/docs/en/agents-and-tools/tool-use/advisor-tool#capping-advisor-output). ### Capping advisor output Set `max_tokens` on the tool definition to cap the advisor's total output (thinking plus text) per call: ```python Python tools = [ { "type": "advisor_20260301", "name": "advisor", "model": "claude-opus-5", "max_tokens": 2048, } ] ``` ```typescript TypeScript const tools: Anthropic.Beta.Messages.BetaToolUnion[] = [ { type: "advisor_20260301", name: "advisor", model: "claude-opus-5", max_tokens: 2048 } ]; ``` ```csharp C# using Anthropic.Models.Beta.Messages; using Messages = Anthropic.Models.Messages; var tools = new BetaToolUnion[] { new BetaAdvisorTool20260301 { Model = Messages::Model.ClaudeOpus5, MaxTokens = 2048 } }; ``` ```go Go tools := []anthropic.BetaToolUnionParam{ {OfAdvisorTool20260301: &anthropic.BetaAdvisorTool20260301Param{ Model: anthropic.ModelClaudeOpus5, MaxTokens: anthropic.Int(2048), }}, } ``` ```java Java import com.anthropic.models.beta.messages.BetaAdvisorTool20260301; import com.anthropic.models.beta.messages.BetaToolUnion; import com.anthropic.models.messages.Model; List tools = List.of( BetaToolUnion.ofAdvisorTool20260301(BetaAdvisorTool20260301.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(2048L) .build())); ``` ```php PHP $tools = [ [ 'type' => 'advisor_20260301', 'name' => 'advisor', 'model' => 'claude-opus-5', 'max_tokens' => 2048, ], ]; ``` ```ruby Ruby tools = [ { type: "advisor_20260301", name: "advisor", model: "claude-opus-5", max_tokens: 2048 } ] ``` The minimum value is 1024. Setting `max_tokens` above the advisor model's own output cap returns a 400 error. The cap applies to each advisor call independently and is not shared across calls in the same request. This is not a hard truncation alone. The server also passes the advisor its remaining-token budget, so the advisor shapes its response to fit. **Recommended starting point:** `max_tokens: 2048`. In Anthropic's testing on a hard reasoning benchmark (n = 40 per configuration), this reduced mean advisor output by roughly 7x compared with leaving the cap unset, with near-zero truncation and no detectable quality degradation. The minimum value of 1024 reduced output roughly 10x but truncated around 10 percent of calls. Accuracy differences across all configurations were within noise at this sample size. Validate on your own workload. | `max_tokens` | Mean advisor output tokens | Calls truncated | | ------------ | -------------------------- | --------------- | | unset | \~4,200 to 5,900 | n/a | | 2048 | \~630 to 840 | \~0% | | 1024 | \~370 to 480 | \~10% | Hard reasoning tasks elicit substantially longer advisor output than the [typical 1,400 to 1,800 tokens](https://platform.claude.com/docs/en/agents-and-tools/tool-use/advisor-tool#usage-and-billing) quoted earlier for lighter workloads. Use this table to size the savings ratio, not as a universal baseline for advisor output. When the advisor does hit the cap, the result block carries `stop_reason: "max_tokens"` on both [result variants](https://platform.claude.com/docs/en/agents-and-tools/tool-use/advisor-tool#result-variants), whichever advisor model you use. Use `stop_reason` to detect truncated advice and decide whether to raise the cap or let the executor proceed with partial guidance. The API also appends `[Advisor output truncated at max_tokens=2048.]` (naming your cap) to the advice text, so the executor sees the truncation in its own context; with a plaintext `advisor_result` advisor that marker is visible to your client as well. Both signals appear only when you set `max_tokens` on the tool definition. ```json { "type": "advisor_tool_result", "tool_use_id": "srvtoolu_abc123", "content": { "type": "advisor_redacted_result", "encrypted_content": "EqQBCkYIBRgCIiQ3YTAwMjY1Mi1mZjM5LTQ1NGUtODgxNC1kNjNjNTk1ZWI3Y...", "stop_reason": "max_tokens" } } ``` Check `output_tokens` on the corresponding `advisor_message` entry in `usage.iterations` to see how close each call came to its cap. Compared with the [prompt-based approach](https://platform.claude.com/docs/en/agents-and-tools/tool-use/advisor-tool#trimming-advisor-output-length), `max_tokens` is a hard ceiling rather than a soft request. Use `max_tokens` when you need a guaranteed bound for cost or latency. Use the prompt-based approach (or both together) when you want to bias toward brevity without risking a mid-thought cut. ### Pairing with effort settings For coding tasks, pairing a Sonnet executor at medium [effort](https://platform.claude.com/docs/en/build-with-claude/effort) with an Opus advisor achieves intelligence comparable to Sonnet at default effort, at lower cost. For maximum intelligence, keep the executor at default effort. ### Cost control * For conversation-level budgets, count advisor calls client-side. When you reach your cap, remove the advisor tool from `tools`; you do not need to strip `advisor_tool_result` blocks from your message history (see the note in [Multi-turn conversations](https://platform.claude.com/docs/en/agents-and-tools/tool-use/advisor-tool#multi-turn-conversations)). * Enable `caching` only for conversations where you expect three or more advisor calls. ## Model compatibility The executor model (the top-level `model` field) and the advisor model (the `model` field inside the tool definition) must form a valid pair. The advisor must be Claude Sonnet 4.6 or a more capable model, and it must be at least as capable as the executor. Models of equal capability (for example, Claude Opus 4.7 and Claude Opus 4.8) can advise each other. | Executor models | Advisor models | | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Claude Haiku 4.5 (claude-haiku-4-5) | Claude Mythos 5 (claude-mythos-5) Claude Fable 5 (claude-fable-5) Claude Opus 5 (claude-opus-5) Claude Opus 4.8 (claude-opus-4-8) Claude Opus 4.7 (claude-opus-4-7) Claude Opus 4.6 (claude-opus-4-6) Claude Sonnet 5 (claude-sonnet-5) Claude Sonnet 4.6 (claude-sonnet-4-6) | | Claude Sonnet 4.6 (claude-sonnet-4-6) | Claude Mythos 5 (claude-mythos-5) Claude Fable 5 (claude-fable-5) Claude Opus 5 (claude-opus-5) Claude Opus 4.8 (claude-opus-4-8) Claude Opus 4.7 (claude-opus-4-7) Claude Opus 4.6 (claude-opus-4-6) Claude Sonnet 5 (claude-sonnet-5) Claude Sonnet 4.6 (claude-sonnet-4-6) | | Claude Sonnet 5 (claude-sonnet-5) | Claude Mythos 5 (claude-mythos-5) Claude Fable 5 (claude-fable-5) Claude Opus 5 (claude-opus-5) Claude Opus 4.8 (claude-opus-4-8) Claude Opus 4.7 (claude-opus-4-7) Claude Sonnet 5 (claude-sonnet-5) | | Claude Opus 4.6 (claude-opus-4-6) | Claude Mythos 5 (claude-mythos-5) Claude Fable 5 (claude-fable-5) Claude Opus 5 (claude-opus-5) Claude Opus 4.8 (claude-opus-4-8) Claude Opus 4.7 (claude-opus-4-7) Claude Opus 4.6 (claude-opus-4-6) Claude Sonnet 5 (claude-sonnet-5) | | Claude Opus 4.7 (claude-opus-4-7) | Claude Mythos 5 (claude-mythos-5) Claude Fable 5 (claude-fable-5) Claude Opus 5 (claude-opus-5) Claude Opus 4.8 (claude-opus-4-8) Claude Opus 4.7 (claude-opus-4-7) | | Claude Opus 4.8 (claude-opus-4-8) | Claude Mythos 5 (claude-mythos-5) Claude Fable 5 (claude-fable-5) Claude Opus 5 (claude-opus-5) Claude Opus 4.8 (claude-opus-4-8) Claude Opus 4.7 (claude-opus-4-7) | | Claude Opus 5 (claude-opus-5) | Claude Mythos 5 (claude-mythos-5) Claude Fable 5 (claude-fable-5) Claude Opus 5 (claude-opus-5) | | Claude Fable 5 (claude-fable-5) | Claude Mythos 5 (claude-mythos-5) Claude Fable 5 (claude-fable-5) Claude Opus 5 (claude-opus-5) | | Claude Mythos 5 (claude-mythos-5) | Claude Mythos 5 (claude-mythos-5) Claude Fable 5 (claude-fable-5) Claude Opus 5 (claude-opus-5) | If you request an invalid pair, the API returns a `400 invalid_request_error` naming the unsupported combination. ### Platform availability The advisor tool is available in beta on the Claude API and on [Claude Platform on AWS](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws). It is not currently available on Amazon Bedrock, Google Cloud, or Microsoft Foundry. ## Advisor on Claude Managed Agents [Claude Managed Agents](https://platform.claude.com/docs/en/managed-agents/overview) sessions support an advisor as well, configured as part of the agent rather than as a tool definition: add a `{"type": "advisor", "model": ...}` entry to the agent's multiagent roster, and the session's primary thread can consult that model mid-turn. The roster entry takes no `max_uses`, `max_tokens`, or `caching` options, and advice is delivered as thread events on the session's event stream rather than as `advisor_tool_result` blocks in the response. See [Give the session an advisor](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration#give-the-session-an-advisor). ## Next steps Store and retrieve information across conversations with a client-side memory directory. Work with Anthropic-executed tools: server\_tool\_use blocks, pause\_turn continuation, and domain filtering. Directory of Anthropic-provided tools and reference for optional tool definition properties. Control how many tokens Claude uses when responding with the effort parameter, trading off between response thoroughness and token efficiency. --- title: Bash tool url: https://platform.claude.com/docs/en/agents-and-tools/tool-use/bash-tool description: Let Claude request shell commands that your application runs in a persistent bash session and returns as tool results. --- For how zero data retention (ZDR) applies to this feature, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention). The bash tool is a [client tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/how-tool-use-works): Claude doesn't run commands itself. When you include the tool in a request, Claude replies with a `tool_use` block that names the command to run. Your application runs that command in a bash session it owns and returns the output in a `tool_result` block. Your application keeps one bash process alive across tool calls, so state persists between commands. The working directory, environment variables, and any files a command creates are still there for the next command. The current version of the tool is `bash_20250124`. For model support, beta headers, and the earlier version, see [Tool versions](https://platform.claude.com/docs/en/agents-and-tools/tool-use/bash-tool#tool-versions). For all Anthropic-provided tools, see the [Tool reference](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-reference). ## Use cases * **Development workflows:** Run build commands, tests, and development tools * **System automation:** Execute scripts, manage files, automate tasks * **Data processing:** Process files, run analysis scripts, manage datasets * **Environment setup:** Install packages, configure environments ## Quick start ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "tools": [ { "type": "bash_20250124", "name": "bash" } ], "messages": [ { "role": "user", "content": "List all Python files in the current directory." } ] }' ``` ```bash CLI ant messages create \ --model claude-opus-5 \ --max-tokens 1024 \ --tool '{type: bash_20250124, name: bash}' \ --message '{role: user, content: List all Python files in the current directory.}' ``` ```python Python client = anthropic.Anthropic() response = client.messages.create( model="claude-opus-5", max_tokens=1024, tools=[{"type": "bash_20250124", "name": "bash"}], messages=[ {"role": "user", "content": "List all Python files in the current directory."} ], ) print(response) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, tools: [{ type: "bash_20250124", name: "bash" }], messages: [ { role: "user", content: "List all Python files in the current directory." } ] }); console.log(response); ``` ```csharp C# var client = new AnthropicClient(); var response = await client.Messages.Create( new() { Model = Model.ClaudeOpus5, MaxTokens = 1024, Tools = [new ToolBash20250124()], Messages = [ new() { Role = Role.User, Content = "List all Python files in the current directory.", }, ], } ); Console.WriteLine(response); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Tools: []anthropic.ToolUnionParam{ {OfBashTool20250124: &anthropic.ToolBash20250124Param{}}, }, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("List all Python files in the current directory.")), }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java import com.anthropic.models.messages.ToolBash20250124; void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); Message response = client.messages().create( MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .addTool(ToolBash20250124.builder().build()) .addUserMessage("List all Python files in the current directory.") .build() ); IO.println(response); } ``` ```php PHP use Anthropic\Messages\ToolBash20250124; $client = new Client(); $response = $client->messages->create( model: 'claude-opus-5', maxTokens: 1024, tools: [new ToolBash20250124()], messages: [ ['role' => 'user', 'content' => 'List all Python files in the current directory.'], ], ); echo $response; ``` ```ruby Ruby client = Anthropic::Client.new response = client.messages.create( model: "claude-opus-5", max_tokens: 1024, tools: [{type: "bash_20250124", name: "bash"}], messages: [ {role: "user", content: "List all Python files in the current directory."} ] ) puts response ``` Claude responds with `stop_reason: "tool_use"` and a `tool_use` block that contains the command for your application to run: ```json Output { "id": "msg_01XAbCDeFgHiJkLmNoPQrStU", "model": "claude-opus-5", "stop_reason": "tool_use", "role": "assistant", "content": [ { "type": "text", "text": "I'll list all Python files in the current directory for you." }, { "type": "tool_use", "id": "toolu_01A09q90qw90lq917835lq9", "name": "bash", "input": { "command": "ls *.py" } } ] } ``` Run `input.command` in your bash session and send the output back as a `tool_result`. See [Implement the bash tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/bash-tool#implement-the-bash-tool) for the round trip. ## How it works Each tool call is one round trip between Claude and your application: 1. Claude returns a `tool_use` block containing the `command` to run. 2. Your application runs the command in its bash session. 3. Your application returns the command's output, stdout and stderr together, to Claude in a `tool_result` block. 4. Claude either requests another command in the same session or responds with text. Claude can also return several `tool_use` blocks in one response. Run them in order in the same session and return all of the results in one `user` message. See [Parallel tool use](https://platform.claude.com/docs/en/agents-and-tools/tool-use/parallel-tool-use). The API is stateless. Nothing about your shell session travels between requests, so your application decides when the session starts, how long it lives, and when to restart it. For the full request and response cycle, see [Handle tool calls](https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls). ## Parameters A bash tool definition has two required fields, `type` and `name`, and the `name` must be `bash`. The tool is schema-less: you don't provide an `input_schema`, because the schema is built into Claude's model and can't be modified. The following table lists the input fields Claude sets when it calls the tool. | Parameter | Required | Description | | --------- | -------- | ----------------------------------------- | | `command` | Yes\* | The bash command to run | | `restart` | No | Set to `true` to restart the bash session | \*Required unless using `restart` To handle `restart: true`, kill the shell process, start a new one, and return a `tool_result` that confirms the restart. A restarted session starts clean: the working directory, environment variables, and any running processes are gone. Run a command: ```json { "command": "ls -la *.py" } ``` Restart the session: ```json { "restart": true } ``` ## Tool versions `bash_20250124` is the current version of the tool, and it requires no beta header. Every model from Claude Sonnet 3.7 ([retired](https://platform.claude.com/docs/en/about-claude/model-deprecations)) onward accepts it, including all current Claude models. The original `bash_20241022` version is part of the computer use beta, and the October 2024 Claude Sonnet 3.5 release ([retired](https://platform.claude.com/docs/en/about-claude/model-deprecations)) is the only model that accepts it. Requests that use it need the `anthropic-beta: computer-use-2024-10-22` header, and the SDKs expose it only in their beta namespaces. New integrations should use `bash_20250124`. ## Example: Multistep automation Claude can chain commands across tool calls to complete a multistep task: ```text User request: "Install the requests library and create a simple Python script that fetches a joke from an API, then run it." Claude's tool uses: 1. Install package {"command": "pip install requests"} 2. Create script {"command": "cat > fetch_joke.py << 'EOF'\nimport requests\nresponse = requests.get('https://official-joke-api.appspot.com/random_joke')\njoke = response.json()\nprint(f\"Setup: {joke['setup']}\")\nprint(f\"Punchline: {joke['punchline']}\")\nEOF"} 3. Run script {"command": "python fetch_joke.py"} ``` The session maintains state between commands, so files created in step 2 are available in step 3. ## Implement the bash tool Claude determines which command to run. Your application owns everything else: the shell process, the timeout, and the safety checks. The following steps show a minimal implementation. Start one long-lived bash process and run every command inside it. Because a pipe to a live process never reports end-of-file, the session prints a unique sentinel line after each command to mark where that command's output ends: ```python Python import subprocess import uuid class BashSession: """A bash process that stays alive between commands so state persists.""" def __init__(self): self.process = subprocess.Popen( ["/bin/bash"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, # interleave errors with output, in order start_new_session=True, # own process group: a timeout can kill every child text=True, ) def execute_command(self, command): """Run a command in the session and return its output.""" sentinel = f"__CLAUDE_BASH_DONE_{uuid.uuid4().hex}__" # unique per call self.process.stdin.write(f"{command}\necho {sentinel}\n") self.process.stdin.flush() output = [] for line in self.process.stdout: if sentinel in line: # this command's output is complete break output.append(line) return "".join(output) def restart(self): self.process.kill() self.process.wait() self.__init__() bash_session = BashSession() print(bash_session.execute_command("cd /tmp && pwd")) print(bash_session.execute_command("pwd")) # still /tmp: the session kept its state ``` ```typescript TypeScript import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; import { createInterface, type Interface } from "node:readline"; import { randomUUID } from "node:crypto"; // A bash process that stays alive between commands so state persists. class BashSession { process!: ChildProcessWithoutNullStreams; private lines!: Interface; constructor() { this.start(); } private start(): void { this.process = spawn("/bin/bash", { detached: true // own process group: a timeout can kill every child }); this.process.stdin.write("exec 2>&1\n"); // interleave errors with output, in order this.lines = createInterface({ input: this.process.stdout }); } // Run a command in the session and return its output. executeCommand(command: string): Promise { const sentinel = `__CLAUDE_BASH_DONE_${randomUUID()}__`; // unique per call const output: string[] = []; const result = new Promise((resolve) => { const onLine = (line: string): void => { if (line.includes(sentinel)) { // this command's output is complete this.lines.off("line", onLine); resolve(output.join("")); } else { output.push(`${line}\n`); } }; this.lines.on("line", onLine); }); this.process.stdin.write(`${command}\necho ${sentinel}\n`); return result; } restart(): void { this.process.kill("SIGKILL"); this.lines.close(); this.start(); } } const session = new BashSession(); console.log(await session.executeCommand("cd /tmp && pwd")); console.log(await session.executeCommand("pwd")); // still /tmp: the session kept its state session.process.stdin.end(); // closing stdin ends the shell so the script can exit ``` ```csharp C# using System.Diagnostics; using System.Text; var session = new BashSession(); Console.Write(session.ExecuteCommand("cd /tmp && pwd")); Console.Write(session.ExecuteCommand("pwd")); // still /tmp: the session kept its state // A bash process that stays alive between commands so state persists. class BashSession { public Process Process { get; private set; } public BashSession() { Process = Start(); } static Process Start() { var process = Process.Start(new ProcessStartInfo("/bin/bash") { RedirectStandardInput = true, RedirectStandardOutput = true })!; process.StandardInput.Write("exec 2>&1\n"); // interleave errors with output, in order process.StandardInput.Flush(); return process; } // Run a command in the session and return its output. public string ExecuteCommand(string command) { var sentinel = $"__CLAUDE_BASH_DONE_{Guid.NewGuid():N}__"; // unique per call Process.StandardInput.Write($"{command}\necho {sentinel}\n"); Process.StandardInput.Flush(); var output = new StringBuilder(); while (Process.StandardOutput.ReadLine() is string line) { if (line.Contains(sentinel)) // this command's output is complete { break; } output.Append(line).Append('\n'); } return output.ToString(); } public void Restart() { Process.Kill(entireProcessTree: true); Process.WaitForExit(); Process = Start(); } } ``` ```go Go import ( "bufio" "crypto/rand" "encoding/hex" "fmt" "io" "log" "os/exec" "strings" "syscall" ) // BashSession is a bash process that stays alive between commands so state persists. type BashSession struct { cmd *exec.Cmd stdin io.WriteCloser output *bufio.Reader } func NewBashSession() (*BashSession, error) { cmd := exec.Command("/bin/bash") cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} // own process group: a timeout can kill every child stdin, err := cmd.StdinPipe() if err != nil { return nil, err } stdout, err := cmd.StdoutPipe() if err != nil { return nil, err } cmd.Stderr = cmd.Stdout // interleave errors with output, in order if err := cmd.Start(); err != nil { return nil, err } return &BashSession{cmd: cmd, stdin: stdin, output: bufio.NewReader(stdout)}, nil } // ExecuteCommand runs a command in the session and returns its output. func (s *BashSession) ExecuteCommand(command string) string { buf := make([]byte, 16) rand.Read(buf) sentinel := fmt.Sprintf("__CLAUDE_BASH_DONE_%s__", hex.EncodeToString(buf)) // unique per call fmt.Fprintf(s.stdin, "%s\necho %s\n", command, sentinel) var output strings.Builder for { line, err := s.output.ReadString('\n') if err != nil || strings.Contains(line, sentinel) { // this command's output is complete break } output.WriteString(line) } return output.String() } // Restart kills the shell and starts a fresh session in its place. func (s *BashSession) Restart() error { s.cmd.Process.Kill() s.cmd.Wait() fresh, err := NewBashSession() if err != nil { return err } *s = *fresh return nil } func main() { session, err := NewBashSession() if err != nil { log.Fatal(err) } fmt.Print(session.ExecuteCommand("cd /tmp && pwd")) fmt.Print(session.ExecuteCommand("pwd")) // still /tmp: the session kept its state } ``` ```java Java import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.util.UUID; // A bash process that stays alive between commands so state persists. class BashSession { Process process; BufferedWriter stdin; BufferedReader output; BashSession() throws IOException { start(); } void start() throws IOException { ProcessBuilder builder = new ProcessBuilder("/bin/bash"); builder.redirectErrorStream(true); // interleave errors with output, in order process = builder.start(); stdin = new BufferedWriter(new OutputStreamWriter(process.getOutputStream())); output = new BufferedReader(new InputStreamReader(process.getInputStream())); } // Run a command in the session and return its output. String executeCommand(String command) throws IOException { String sentinel = "__CLAUDE_BASH_DONE_" + UUID.randomUUID() + "__"; // unique per call stdin.write(command + "\necho " + sentinel + "\n"); stdin.flush(); StringBuilder result = new StringBuilder(); String line; while ((line = output.readLine()) != null) { if (line.contains(sentinel)) { // this command's output is complete break; } result.append(line).append("\n"); } return result.toString(); } void restart() throws IOException, InterruptedException { process.destroyForcibly(); process.waitFor(); start(); } } void main() throws Exception { BashSession session = new BashSession(); IO.println(session.executeCommand("cd /tmp && pwd")); IO.println(session.executeCommand("pwd")); // still /tmp: the session kept its state } ``` ```php PHP // A bash process that stays alive between commands so state persists. class BashSession { public $process; public $stdin; public $output; public function __construct() { $this->start(); } private function start(): void { // setsid gives the shell its own process group: a timeout can kill every child $this->process = proc_open( ['setsid', '/bin/bash'], [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['redirect', 1]], // interleave errors with output $pipes ); $this->stdin = $pipes[0]; $this->output = $pipes[1]; } // Run a command in the session and return its output. public function executeCommand(string $command): string { $sentinel = '__CLAUDE_BASH_DONE_' . bin2hex(random_bytes(16)) . '__'; // unique per call fwrite($this->stdin, "{$command}\necho {$sentinel}\n"); fflush($this->stdin); $output = ''; while (($line = fgets($this->output)) !== false) { if (str_contains($line, $sentinel)) { // this command's output is complete break; } $output .= $line; } return $output; } public function restart(): void { proc_terminate($this->process, 9); proc_close($this->process); $this->start(); } } $session = new BashSession(); echo $session->executeCommand("cd /tmp && pwd"); echo $session->executeCommand("pwd"); // still /tmp: the session kept its state ``` ```ruby Ruby require "open3" require "securerandom" # A bash process that stays alive between commands so state persists. class BashSession attr_reader :output, :wait_thread def initialize start end # Run a command in the session and return its output. def execute_command(command) sentinel = "__CLAUDE_BASH_DONE_#{SecureRandom.hex(16)}__" # unique per call @stdin.write("#{command}\necho #{sentinel}\n") @stdin.flush output = +"" @output.each_line do |line| break if line.include?(sentinel) # this command's output is complete output << line end output end def restart Process.kill("KILL", @wait_thread.pid) @wait_thread.join start end private def start # popen2e interleaves errors with output, in order; pgroup gives the shell its # own process group so a timeout can kill every child @stdin, @output, @wait_thread = Open3.popen2e("/bin/bash", pgroup: true) end end session = BashSession.new puts session.execute_command("cd /tmp && pwd") puts session.execute_command("pwd") # still /tmp: the session kept its state ``` The session interleaves stderr with stdout, so error messages land where they happened. The example leaves out what a complete implementation also needs: a timeout that kills the shell and every process it started when a command hangs, then restarts the session. The [Use command timeouts](https://platform.claude.com/docs/en/agents-and-tools/tool-use/bash-tool#follow-implementation-best-practices) best practice shows one way to add it. Extract and run commands from Claude's responses: ```python Python tool_results = [] for content in response.content: if content.type == "tool_use" and content.name == "bash": if content.input.get("restart"): bash_session.restart() result = "Bash session restarted" else: command = content.input.get("command") result = bash_session.execute_command(command) # One tool_result per tool_use block, all returned in the next user message tool_results.append( {"type": "tool_result", "tool_use_id": content.id, "content": result} ) ``` ```typescript TypeScript const toolResults: { type: string; tool_use_id: string; content: string }[] = []; for (const block of response.content) { if (block.type === "tool_use" && block.name === "bash") { let result: string; if (block.input.restart) { bashSession.restart(); result = "Bash session restarted"; } else { result = await bashSession.executeCommand(block.input.command ?? ""); } // One tool_result per tool_use block, all returned in the next user message toolResults.push({ type: "tool_result", tool_use_id: block.id, content: result }); } } ``` ```csharp C# var toolResults = new List(); foreach (var block in response.Content) { if (block.TryPickToolUse(out var toolUse) && toolUse.Name == "bash") { string result; if (toolUse.Input.TryGetValue("restart", out var restart) && restart.GetBoolean()) { bashSession.Restart(); result = "Bash session restarted"; } else { var command = toolUse.Input["command"].GetString() ?? ""; result = bashSession.ExecuteCommand(command); } // One tool_result per tool_use block, all returned in the next user message toolResults.Add(new ToolResultBlockParam { ToolUseID = toolUse.ID, Content = result }); } } ``` ```go Go var toolResults []anthropic.ContentBlockParamUnion for _, block := range response.Content { if block.Type == "tool_use" && block.Name == "bash" { var input struct { Command string `json:"command"` Restart bool `json:"restart"` } if err := json.Unmarshal(block.Input, &input); err != nil { log.Fatal(err) } var result string if input.Restart { bashSession.Restart() result = "Bash session restarted" } else { result = bashSession.ExecuteCommand(input.Command) } // One tool_result per tool_use block, all returned in the next user message toolResults = append(toolResults, anthropic.NewToolResultBlock(block.ID, result, false)) } } ``` ```java Java List> toolResults = new ArrayList<>(); for (ContentBlock block : response.content()) { if (block.type().equals("tool_use") && block.name().equals("bash")) { String result; if (Boolean.TRUE.equals(block.input().get("restart"))) { bashSession.restart(); result = "Bash session restarted"; } else { String command = (String) block.input().get("command"); result = bashSession.executeCommand(command); } // One tool_result per tool_use block, all returned in the next user message toolResults.add(Map.of("type", "tool_result", "tool_use_id", block.id(), "content", result)); } } ``` ```php PHP $toolResults = []; foreach ($response->content as $block) { if ($block->type === 'tool_use' && $block->name === 'bash') { if (!empty($block->input['restart'])) { $bashSession->restart(); $result = 'Bash session restarted'; } else { $result = $bashSession->executeCommand($block->input['command']); } // One tool_result per tool_use block, all returned in the next user message $toolResults[] = ['type' => 'tool_result', 'tool_use_id' => $block->id, 'content' => $result]; } } ``` ```ruby Ruby tool_results = [] response.content.each do |block| next unless block.type == :tool_use && block.name == "bash" result = if block.input[:restart] bash_session.restart "Bash session restarted" else bash_session.execute_command(block.input[:command]) end # One tool_result per tool_use block, all returned in the next user message tool_results << {type: "tool_result", tool_use_id: block.id, content: result} end ``` Send the `tool_result` back in a `user` message that continues the same conversation. Claude either requests another command in the same session or finishes its answer: ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "tools": [ { "type": "bash_20250124", "name": "bash" } ], "messages": [ { "role": "user", "content": "List all Python files in the current directory." }, { "role": "assistant", "content": [ { "type": "tool_use", "id": "toolu_01A09q90qw90lq917835lq9", "name": "bash", "input": { "command": "ls *.py" } } ] }, { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_01A09q90qw90lq917835lq9", "content": "analysis.py\nprocess_data.py\n" } ] } ] }' ``` ```bash CLI ant messages create <<'YAML' model: claude-opus-5 max_tokens: 1024 tools: - type: bash_20250124 name: bash messages: - role: user content: List all Python files in the current directory. - role: assistant content: - type: tool_use id: toolu_01A09q90qw90lq917835lq9 name: bash input: command: ls *.py - role: user content: - type: tool_result tool_use_id: toolu_01A09q90qw90lq917835lq9 content: | analysis.py process_data.py YAML ``` ```python Python client = anthropic.Anthropic() response = client.messages.create( model="claude-opus-5", max_tokens=1024, tools=[{"type": "bash_20250124", "name": "bash"}], messages=[ {"role": "user", "content": "List all Python files in the current directory."}, { "role": "assistant", "content": [ { "type": "tool_use", "id": "toolu_01A09q90qw90lq917835lq9", "name": "bash", "input": {"command": "ls *.py"}, } ], }, { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_01A09q90qw90lq917835lq9", "content": "analysis.py\nprocess_data.py\n", } ], }, ], ) print(response.content) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, tools: [{ type: "bash_20250124", name: "bash" }], messages: [ { role: "user", content: "List all Python files in the current directory." }, { role: "assistant", content: [ { type: "tool_use", id: "toolu_01A09q90qw90lq917835lq9", name: "bash", input: { command: "ls *.py" } } ] }, { role: "user", content: [ { type: "tool_result", tool_use_id: "toolu_01A09q90qw90lq917835lq9", content: "analysis.py\nprocess_data.py\n" } ] } ] }); console.log(response.content); ``` ```csharp C# var client = new AnthropicClient(); var response = await client.Messages.Create( new() { Model = Model.ClaudeOpus5, MaxTokens = 1024, Tools = [new ToolBash20250124()], Messages = [ new() { Role = Role.User, Content = "List all Python files in the current directory.", }, new() { Role = Role.Assistant, Content = new MessageParamContent(new List { new ContentBlockParam(new ToolUseBlockParam() { ID = "toolu_01A09q90qw90lq917835lq9", Name = "bash", Input = new Dictionary { ["command"] = JsonSerializer.SerializeToElement("ls *.py"), }, }), }), }, new() { Role = Role.User, Content = new MessageParamContent(new List { new ContentBlockParam(new ToolResultBlockParam() { ToolUseID = "toolu_01A09q90qw90lq917835lq9", Content = "analysis.py\nprocess_data.py\n", }), }), }, ], } ); Console.WriteLine(response); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Tools: []anthropic.ToolUnionParam{ {OfBashTool20250124: &anthropic.ToolBash20250124Param{}}, }, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("List all Python files in the current directory.")), anthropic.NewAssistantMessage( anthropic.NewToolUseBlock( "toolu_01A09q90qw90lq917835lq9", map[string]any{"command": "ls *.py"}, "bash", ), ), anthropic.NewUserMessage( anthropic.NewToolResultBlock( "toolu_01A09q90qw90lq917835lq9", "analysis.py\nprocess_data.py\n", false, ), ), }, }) if err != nil { log.Fatal(err) } fmt.Println(response.Content) ``` ```java Java import com.anthropic.core.JsonValue; import com.anthropic.models.messages.ContentBlockParam; // ... import com.anthropic.models.messages.ToolBash20250124; import com.anthropic.models.messages.ToolResultBlockParam; import com.anthropic.models.messages.ToolUseBlockParam; // ... void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .addTool(ToolBash20250124.builder().build()) .addUserMessage("List all Python files in the current directory.") .addAssistantMessageOfBlockParams( List.of( ContentBlockParam.ofToolUse( ToolUseBlockParam.builder() .id("toolu_01A09q90qw90lq917835lq9") .name("bash") .input( ToolUseBlockParam.Input.builder() .putAdditionalProperty("command", JsonValue.from("ls *.py")) .build() ) .build() ) ) ) .addUserMessageOfBlockParams( List.of( ContentBlockParam.ofToolResult( ToolResultBlockParam.builder() .toolUseId("toolu_01A09q90qw90lq917835lq9") .content("analysis.py\nprocess_data.py\n") .build() ) ) ) .build(); Message response = client.messages().create(params); IO.println(response.content()); } ``` ```php PHP use Anthropic\Messages\ToolBash20250124; $client = new Client(); $response = $client->messages->create( model: 'claude-opus-5', maxTokens: 1024, tools: [new ToolBash20250124()], messages: [ ['role' => 'user', 'content' => 'List all Python files in the current directory.'], [ 'role' => 'assistant', 'content' => [ [ 'type' => 'tool_use', 'id' => 'toolu_01A09q90qw90lq917835lq9', 'name' => 'bash', 'input' => ['command' => 'ls *.py'], ], ], ], [ 'role' => 'user', 'content' => [ [ 'type' => 'tool_result', 'tool_use_id' => 'toolu_01A09q90qw90lq917835lq9', 'content' => "analysis.py\nprocess_data.py\n", ], ], ], ], ); print_r($response->content); ``` ```ruby Ruby client = Anthropic::Client.new response = client.messages.create( model: "claude-opus-5", max_tokens: 1024, tools: [{type: "bash_20250124", name: "bash"}], messages: [ {role: "user", content: "List all Python files in the current directory."}, { role: "assistant", content: [ { type: "tool_use", id: "toolu_01A09q90qw90lq917835lq9", name: "bash", input: {command: "ls *.py"} } ] }, { role: "user", content: [ { type: "tool_result", tool_use_id: "toolu_01A09q90qw90lq917835lq9", content: "analysis.py\nprocess_data.py\n" } ] } ] ) puts response.content ``` Repeat the run-and-return cycle while `stop_reason` is `tool_use`. For the full loop, see [Handling results from client tools](https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls#handling-results-from-client-tools). Add validation and restrictions. Use an allowlist rather than a blocklist: a blocklist misses any command it didn't anticipate. The example also rejects shell operators that appear as separate words: ```python Python import shlex ALLOWED_COMMANDS = {"ls", "cat", "echo", "pwd", "grep", "find", "wc", "head", "tail"} SHELL_OPERATORS = {"&&", "||", "|", ";", "&", ">", "<", ">>"} def validate_command(command): # Allow only commands from an explicit allowlist try: tokens = shlex.split(command) except ValueError: return False, "Could not parse command" if not tokens: return False, "Empty command" executable = tokens[0] if executable not in ALLOWED_COMMANDS: return False, f"Command '{executable}' is not in the allowlist" # Reject shell operators written as separate words for token in tokens[1:]: if token in SHELL_OPERATORS or token.startswith(("$", "`")): return False, f"Shell operator '{token}' is not allowed" return True, None ``` ```typescript TypeScript const ALLOWED_COMMANDS = new Set([ "ls", "cat", "echo", "pwd", "grep", "find", "wc", "head", "tail" ]); const SHELL_OPERATORS = new Set(["&&", "||", "|", ";", "&", ">", "<", ">>"]); function validateCommand(command: string): { ok: boolean; reason?: string } { // Split on whitespace: enough for a tripwire check const tokens = command.split(/\s+/).filter((token) => token.length > 0); if (tokens.length === 0) { return { ok: false, reason: "Empty command" }; } // Allow only commands from an explicit allowlist const executable = tokens[0]; if (!ALLOWED_COMMANDS.has(executable)) { return { ok: false, reason: `Command '${executable}' is not in the allowlist` }; } // Reject shell operators written as separate words for (const token of tokens.slice(1)) { const bare = token.replace(/^["']+/, ""); // a quoted token can still smuggle an expansion if (SHELL_OPERATORS.has(token) || bare.startsWith("$") || bare.startsWith("`")) { return { ok: false, reason: `Shell operator '${token}' is not allowed` }; } } return { ok: true }; } ``` ```csharp C# var allowedCommands = new HashSet { "ls", "cat", "echo", "pwd", "grep", "find", "wc", "head", "tail" }; var shellOperators = new HashSet { "&&", "||", "|", ";", "&", ">", "<", ">>" }; (bool Ok, string? Reason) ValidateCommand(string command) { // Split on whitespace: enough for a tripwire check var tokens = command.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries); if (tokens.Length == 0) { return (false, "Empty command"); } // Allow only commands from an explicit allowlist var executable = tokens[0]; if (!allowedCommands.Contains(executable)) { return (false, $"Command '{executable}' is not in the allowlist"); } // Reject shell operators written as separate words foreach (var token in tokens.Skip(1)) { var bare = token.TrimStart('"', '\''); // a quoted token can still smuggle an expansion if (shellOperators.Contains(token) || bare.StartsWith('$') || bare.StartsWith('`')) { return (false, $"Shell operator '{token}' is not allowed"); } } return (true, null); } ``` ```go Go var allowedCommands = map[string]bool{ "ls": true, "cat": true, "echo": true, "pwd": true, "grep": true, "find": true, "wc": true, "head": true, "tail": true, } var shellOperators = map[string]bool{ "&&": true, "||": true, "|": true, ";": true, "&": true, ">": true, "<": true, ">>": true, } func validateCommand(command string) (bool, string) { // Split on whitespace: enough for a tripwire check tokens := strings.Fields(command) if len(tokens) == 0 { return false, "Empty command" } // Allow only commands from an explicit allowlist executable := tokens[0] if !allowedCommands[executable] { return false, fmt.Sprintf("Command %q is not in the allowlist", executable) } // Reject shell operators written as separate words for _, token := range tokens[1:] { bare := strings.TrimLeft(token, `"'`) // a quoted token can still smuggle an expansion if shellOperators[token] || strings.HasPrefix(bare, "$") || strings.HasPrefix(bare, "`") { return false, fmt.Sprintf("Shell operator %q is not allowed", token) } } return true, "" } ``` ```java Java import java.util.List; import java.util.Set; static final Set ALLOWED_COMMANDS = Set.of("ls", "cat", "echo", "pwd", "grep", "find", "wc", "head", "tail"); static final Set SHELL_OPERATORS = Set.of("&&", "||", "|", ";", "&", ">", "<", ">>"); record Validation(boolean ok, String reason) {} Validation validateCommand(String command) { // Split on whitespace: enough for a tripwire check List tokens = List.of(command.trim().split("\\s+")); if (tokens.size() == 1 && tokens.get(0).isEmpty()) { return new Validation(false, "Empty command"); } // Allow only commands from an explicit allowlist String executable = tokens.get(0); if (!ALLOWED_COMMANDS.contains(executable)) { return new Validation(false, "Command '" + executable + "' is not in the allowlist"); } // Reject shell operators written as separate words for (String token : tokens.subList(1, tokens.size())) { String bare = token.replaceFirst("^[\"']+", ""); // a quoted token can still smuggle an expansion if (SHELL_OPERATORS.contains(token) || bare.startsWith("$") || bare.startsWith("`")) { return new Validation(false, "Shell operator '" + token + "' is not allowed"); } } return new Validation(true, null); } ``` ```php PHP const ALLOWED_COMMANDS = ['ls', 'cat', 'echo', 'pwd', 'grep', 'find', 'wc', 'head', 'tail']; const SHELL_OPERATORS = ['&&', '||', '|', ';', '&', '>', '<', '>>']; function validateCommand(string $command): array { // Split on whitespace: enough for a tripwire check $tokens = preg_split('/\\s+/', trim($command), -1, PREG_SPLIT_NO_EMPTY); if ($tokens === false || $tokens === []) { return [false, 'Empty command']; } // Allow only commands from an explicit allowlist $executable = $tokens[0]; if (!in_array($executable, ALLOWED_COMMANDS, true)) { return [false, "Command '{$executable}' is not in the allowlist"]; } // Reject shell operators written as separate words foreach (array_slice($tokens, 1) as $token) { $bare = ltrim($token, '"\''); // a quoted token can still smuggle an expansion if (in_array($token, SHELL_OPERATORS, true) || str_starts_with($bare, '$') || str_starts_with($bare, '`')) { return [false, "Shell operator '{$token}' is not allowed"]; } } return [true, null]; } ``` ```ruby Ruby require "shellwords" ALLOWED_COMMANDS = %w[ls cat echo pwd grep find wc head tail].freeze SHELL_OPERATORS = ["&&", "||", "|", ";", "&", ">", "<", ">>"].freeze def validate_command(command) # Allow only commands from an explicit allowlist begin tokens = Shellwords.split(command) rescue ArgumentError return [false, "Could not parse command"] end return [false, "Empty command"] if tokens.empty? executable = tokens[0] unless ALLOWED_COMMANDS.include?(executable) return [false, "Command '#{executable}' is not in the allowlist"] end # Reject shell operators written as separate words tokens[1..].each do |token| if SHELL_OPERATORS.include?(token) || token.start_with?("$", "`") return [false, "Shell operator '#{token}' is not allowed"] end end [true, nil] end ``` This check is a tripwire for obvious mistakes, not an enforcement boundary. It rejects the spaced chaining (`&&`), pipes, and redirection that the other examples on this page use. It does not catch an operator glued to a word, such as `cat data.txt|grep x`, because the tokenizer keeps `data.txt|grep` inside one token. Decide which commands and operators your application allows. The real control is isolation: run the whole session inside a container or a virtual machine (see [Security](https://platform.claude.com/docs/en/agents-and-tools/tool-use/bash-tool#security)). ### Handle errors When a command fails or the session breaks, tell Claude what happened. Return the message as the `tool_result` content and set `is_error` to `true`, which marks the tool call as failed. See [Handling errors with is\_error](https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls#handling-errors-with-is-error). If a command takes too long to execute: ```json { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_01A09q90qw90lq917835lq9", "content": "Error: command did not finish within 30 seconds", "is_error": true } ] } ``` If a command doesn't exist: ```json { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_01A09q90qw90lq917835lq9", "content": "bash: nonexistentcommand: command not found", "is_error": true } ] } ``` If there are permission issues: ```json { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_01A09q90qw90lq917835lq9", "content": "bash: /root/sensitive-file: Permission denied", "is_error": true } ] } ``` ### Follow implementation best practices A command that never finishes, such as one that waits for input, blocks the session forever because its sentinel line never arrives. Give every command a deadline. When the deadline passes, stop the shell and everything the command started, then restart the session: ```python Python import concurrent.futures import os import signal def execute_with_timeout(session, command, timeout=30): """Run a command in the session, replacing the session if the command hangs.""" with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool: future = pool.submit(session.execute_command, command) try: return future.result(timeout=timeout) except concurrent.futures.TimeoutError: # The group is the shell and every process the command started os.killpg(session.process.pid, signal.SIGKILL) session.restart() return f"Error: command did not finish within {timeout} seconds" ``` ```typescript TypeScript // Run a command in the session, replacing the session if the command hangs. async function executeWithTimeout( session: BashSession, command: string, timeoutMs = 30000 ): Promise { let timer: NodeJS.Timeout | undefined; const timedOut = new Promise((_, reject) => { timer = setTimeout(() => reject(new Error("timeout")), timeoutMs); }); try { return await Promise.race([session.executeCommand(command), timedOut]); } catch { // The group is the shell and every process the command started if (session.process.pid !== undefined) { process.kill(-session.process.pid, "SIGKILL"); } session.restart(); return `Error: command did not finish within ${timeoutMs / 1000} seconds`; } finally { clearTimeout(timer); } } ``` ```csharp C# using System.Diagnostics; // Run a command in the session, replacing the session if the command hangs. static string ExecuteWithTimeout(BashSession session, string command, int timeoutSeconds = 30) { var work = Task.Run(() => session.ExecuteCommand(command)); if (work.Wait(TimeSpan.FromSeconds(timeoutSeconds))) { return work.Result; } // Stop the shell and every process it started, then start a fresh session session.Process.Kill(entireProcessTree: true); session.Restart(); return $"Error: command did not finish within {timeoutSeconds} seconds"; } ``` ```go Go // executeWithTimeout runs a command, replacing the session if the command hangs. func executeWithTimeout(session *BashSession, command string, timeoutSeconds int) string { done := make(chan string, 1) go func() { done <- session.ExecuteCommand(command) }() select { case result := <-done: return result case <-time.After(time.Duration(timeoutSeconds) * time.Second): // The group is the shell and every process the command started syscall.Kill(-session.cmd.Process.Pid, syscall.SIGKILL) session.Restart() return fmt.Sprintf("Error: command did not finish within %d seconds", timeoutSeconds) } } ``` ```java Java // Run a command in the session, replacing the session if the command hangs. String executeWithTimeout(BashSession session, String command, int timeoutSeconds) throws Exception { ExecutorService pool = Executors.newSingleThreadExecutor(); try { Future future = pool.submit(() -> session.executeCommand(command)); return future.get(timeoutSeconds, TimeUnit.SECONDS); } catch (TimeoutException e) { // Stop the shell and every process it started, then start a fresh session session.process.descendants().forEach(ProcessHandle::destroyForcibly); session.process.destroyForcibly(); session.restart(); return "Error: command did not finish within " + timeoutSeconds + " seconds"; } finally { pool.shutdownNow(); } } ``` ```php PHP // Run a command but give up if it does not finish within the deadline. PHP blocks on // pipe reads, so the deadline lives inside the read loop: stream_select() waits for // readable output before each fgets() so the loop can check the deadline. function executeWithTimeout(BashSession $session, string $command, int $timeout = 30): string { $sentinel = '__CLAUDE_BASH_DONE_' . bin2hex(random_bytes(16)) . '__'; // unique per call fwrite($session->stdin, "{$command}\necho {$sentinel}\n"); fflush($session->stdin); $deadline = microtime(true) + $timeout; $output = ''; while (microtime(true) < $deadline) { $read = [$session->output]; $write = null; $except = null; if (stream_select($read, $write, $except, 1) === 0) { continue; // no output yet; check the deadline again } $line = fgets($session->output); if ($line === false || str_contains($line, $sentinel)) { return $output; // this command's output is complete } $output .= $line; } // The group is the shell and every process the command started posix_kill(-proc_get_status($session->process)['pid'], 9); // 9 = SIGKILL $session->restart(); return "Error: command did not finish within {$timeout} seconds"; } ``` ```ruby Ruby require "timeout" # Run a command in the session, replacing the session if the command hangs. def execute_with_timeout(session, command, timeout: 30) Timeout.timeout(timeout) { session.execute_command(command) } rescue Timeout::Error # The group is the shell and every process the command started Process.kill("KILL", -session.wait_thread.pid) session.restart "Error: command did not finish within #{timeout} seconds" end ``` The kill stops the hung command and everything it started. Return the message as an error `tool_result` (see [Handle errors](https://platform.claude.com/docs/en/agents-and-tools/tool-use/bash-tool#handle-errors)), which marks the tool call as failed. Keep the bash session persistent to maintain environment variables and working directory: ```python Python # Commands run in the same session maintain state commands = [ "cd /tmp", "echo 'Hello' > test.txt", "cat test.txt", # The session is still in /tmp ] ``` ```typescript TypeScript // Commands run in the same session maintain state const commands = [ "cd /tmp", "echo 'Hello' > test.txt", "cat test.txt" // The session is still in /tmp ]; ``` ```csharp C# // Commands run in the same session maintain state string[] commands = [ "cd /tmp", "echo 'Hello' > test.txt", "cat test.txt", // The session is still in /tmp ]; ``` ```go Go // Commands run in the same session maintain state commands := []string{ "cd /tmp", "echo 'Hello' > test.txt", "cat test.txt", // The session is still in /tmp } ``` ```java Java // Commands run in the same session maintain state List commands = List.of( "cd /tmp", "echo 'Hello' > test.txt", "cat test.txt" // The session is still in /tmp ); ``` ```php PHP // Commands run in the same session maintain state $commands = [ 'cd /tmp', "echo 'Hello' > test.txt", 'cat test.txt', // The session is still in /tmp ]; ``` ```ruby Ruby # Commands run in the same session maintain state commands = [ "cd /tmp", "echo 'Hello' > test.txt", "cat test.txt" # The session is still in /tmp ] ``` Truncate large outputs to prevent token limit issues: ```python Python def truncate_output(output, max_lines=100): lines = output.split("\n") if len(lines) > max_lines: truncated = "\n".join(lines[:max_lines]) return f"{truncated}\n\n... Output truncated ({len(lines)} total lines) ..." return output ``` ```typescript TypeScript function truncateOutput(output: string, maxLines = 100): string { const lines = output.split("\n"); if (lines.length > maxLines) { const truncated = lines.slice(0, maxLines).join("\n"); return `${truncated}\n\n... Output truncated (${lines.length} total lines) ...`; } return output; } ``` ```csharp C# string TruncateOutput(string output, int maxLines = 100) { var lines = output.Split('\n'); if (lines.Length > maxLines) { var truncated = string.Join("\n", lines.Take(maxLines)); return $"{truncated}\n\n... Output truncated ({lines.Length} total lines) ..."; } return output; } ``` ```go Go func truncateOutput(output string, maxLines int) string { lines := strings.Split(output, "\n") if len(lines) > maxLines { truncated := strings.Join(lines[:maxLines], "\n") return fmt.Sprintf("%s\n\n... Output truncated (%d total lines) ...", truncated, len(lines)) } return output } ``` ```java Java String truncateOutput(String output, int maxLines) { String[] lines = output.split("\n", -1); if (lines.length > maxLines) { String truncated = String.join("\n", Arrays.copyOf(lines, maxLines)); return truncated + "\n\n... Output truncated (" + lines.length + " total lines) ..."; } return output; } ``` ```php PHP function truncateOutput(string $output, int $maxLines = 100): string { $lines = explode("\n", $output); if (count($lines) > $maxLines) { $truncated = implode("\n", array_slice($lines, 0, $maxLines)); return "{$truncated}\n\n... Output truncated (" . count($lines) . ' total lines) ...'; } return $output; } ``` ```ruby Ruby def truncate_output(output, max_lines: 100) lines = output.split("\n", -1) return output unless lines.length > max_lines truncated = lines.first(max_lines).join("\n") "#{truncated}\n\n... Output truncated (#{lines.length} total lines) ..." end ``` Keep an audit trail. Route every command through one wrapper that records the command before it runs and the output after it finishes. A command that hangs or breaks the session still leaves a record: ```python Python import logging logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s") def execute_and_log(session, command): """Run a command in the session and keep an audit record of it.""" logging.info("command=%r", command) output = session.execute_command(command) logging.info("output=%r", output[:200]) # first 200 characters return output ``` ```typescript TypeScript // Run a command in the session and keep an audit record of it. async function executeAndLog(session: BashSession, command: string): Promise { console.error(`command=${JSON.stringify(command)}`); const output = await session.executeCommand(command); console.error(`output=${JSON.stringify(output.slice(0, 200))}`); // first 200 characters return output; } ``` ```csharp C# // Run a command in the session and keep an audit record of it. static string ExecuteAndLog(BashSession session, string command) { Console.Error.WriteLine($"command={command}"); var output = session.ExecuteCommand(command); Console.Error.WriteLine($"output={output[..Math.Min(output.Length, 200)]}"); // first 200 characters return output; } ``` ```go Go // executeAndLog runs a command in the session and keeps an audit record of it. func executeAndLog(session *BashSession, command string) string { log.Printf("command=%q", command) output := session.ExecuteCommand(command) log.Printf("output=%q", output[:min(len(output), 200)]) // first 200 characters return output } ``` ```java Java static final Logger AUDIT = Logger.getLogger("bash-audit"); // Run a command in the session and keep an audit record of it. String executeAndLog(BashSession session, String command) throws IOException { AUDIT.info("command=" + command); String output = session.executeCommand(command); AUDIT.info("output=" + output.substring(0, Math.min(output.length(), 200))); // first 200 characters return output; } ``` ```php PHP // Run a command in the session and keep an audit record of it. function executeAndLog(BashSession $session, string $command): string { error_log("command={$command}"); $output = $session->executeCommand($command); error_log('output=' . substr($output, 0, 200)); // first 200 characters return $output; } ``` ```ruby Ruby require "logger" AUDIT = Logger.new($stderr) # Run a command in the session and keep an audit record of it. def execute_and_log(session, command) AUDIT.info("command=#{command.inspect}") output = session.execute_command(command) AUDIT.info("output=#{output[0, 200].inspect}") # first 200 characters output end ``` The records go to `stderr` by default; point them at a file or your logging pipeline to keep them. Include whatever ties the record to the request in your application, such as the end user and the `tool_use_id`. ## Security Your application runs whatever command Claude requests. Run the session in an isolated environment, such as a container or a virtual machine, as the least-privileged user that can do the work. Treat every command as untrusted input. Beyond isolation, add these controls: * Validate commands before running them, with an allowlist rather than a blocklist. See [Implement the bash tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/bash-tool#implement-the-bash-tool). * Set resource limits on the shell process (CPU, memory, and disk), for example with `ulimit`. * Log every command and its output so you can audit what ran. * Redact credentials and other secrets from output before returning it to Claude. ## Pricing The bash tool definition adds the following input tokens to your request. This is in addition to the per-model [tool use system prompt](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview#pricing) that applies whenever any tool is present. | Model | Additional input tokens | | --------------------------------------------------- | ----------------------- | | Claude Opus 5, Claude Opus 4.8, and Claude Opus 4.7 | 325 tokens | | Claude Opus 4.6, Claude Sonnet 4.6, and earlier | 244 tokens | Additional tokens are consumed by: * Command outputs (stdout/stderr) * Error messages * Large file contents See [tool use pricing](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview#pricing) for complete pricing details. ## Common patterns ### Development workflows * Running tests: `pytest && coverage report` * Building projects: `npm install && npm run build` * Git operations: `git status && git add . && git commit -m "message"` For guidance on using git as a checkpoint-and-recovery mechanism in long-running agent workflows, see [state management best practices](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-prompting-best-practices#state-management-best-practices). ### File operations * Processing data: `wc -l *.csv && ls -lh *.csv` * Searching files: `find . -name "*.py" | xargs grep "pattern"` * Creating backups: `tar -czf backup.tar.gz ./data` ### System tasks * Checking resources: `df -h && free -m` * Process management: `ps aux | grep python` * Environment setup: `export PATH=$PATH:/new/path && echo $PATH` ## Limitations * **No interactive commands:** The session can't run `vim`, `less`, password prompts, or any command that waits for input on stdin. * **No GUI applications:** The session is command-line only. * **Session scope:** Bash session state is client-side. Your application is responsible for maintaining the shell session between turns. * **Output limits:** The API doesn't truncate tool results (an oversized request is rejected). Truncate large outputs in your application before returning them to Claude. * **No streaming:** Output reaches Claude only when your application returns the `tool_result` in the next request. ## Combining with other tools The bash tool pairs well with the [Text editor tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/text-editor-tool): Claude edits a file with one tool and requests the command that runs it with the other. If you're also using the [Code execution tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool), Claude has access to two separate execution environments: your local bash session and Anthropic's sandboxed container. State is not shared between them. See [Using code execution with other execution tools](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool#using-code-execution-with-other-execution-tools) for guidance on prompting Claude to distinguish between environments. ## Next steps View and modify text files to debug, fix, and improve code. Connect Claude to external tools and APIs. See where tools execute, when Claude calls them, and which tool fits your task. --- title: Code execution tool url: https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool description: Run Python and bash code in a sandboxed container to analyze data, generate files, and iterate on solutions. --- Claude can analyze data, create visualizations, perform complex calculations, run system commands, create and edit files, and process uploaded files directly within the API conversation. The code execution tool allows Claude to run Bash commands and manipulate files, including writing code, in a secure, sandboxed environment. **Code execution is free when used with web search or web fetch (`web_search_20260209`, `web_fetch_20260209`, or later).** When one of those tools is in your request, there are no additional charges for code execution in that request beyond standard token costs. This covers both the code execution behind dynamic filtering and any code Claude runs directly. Standard code execution pricing applies when they are not included. Code execution also powers dynamic filtering in the [web search](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-search-tool) and [web fetch](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-fetch-tool) tools: Claude filters results inside the code execution environment before they reach the context window. When dynamic filtering runs, the API provisions the code execution it needs for the request automatically, so you don't add the code execution tool to your request for it. Reach out through the [feedback form](https://forms.gle/LTAU6Xn2puCJMi1n6) to share your feedback on this feature. For how zero data retention (ZDR) applies to this feature, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention). ## Model compatibility The code execution tool is available on the following models: | Model | Tool versions | | ---------------------------------------------- | ------------------------------------------------------------------------------- | | Claude Opus 5 (claude-opus-5) | `code_execution_20250825`, `code_execution_20260120`, `code_execution_20260521` | | Claude Fable 5 (claude-fable-5) | `code_execution_20250825`, `code_execution_20260120`, `code_execution_20260521` | | Claude Mythos 5 (claude-mythos-5) | `code_execution_20250825`, `code_execution_20260120`, `code_execution_20260521` | | Claude Sonnet 5 (claude-sonnet-5) | `code_execution_20250825`, `code_execution_20260120`, `code_execution_20260521` | | Claude Opus 4.8 (claude-opus-4-8) | `code_execution_20250825`, `code_execution_20260120`, `code_execution_20260521` | | Claude Opus 4.7 (claude-opus-4-7) | `code_execution_20250825`, `code_execution_20260120`, `code_execution_20260521` | | Claude Opus 4.6 (claude-opus-4-6) | `code_execution_20250825`, `code_execution_20260120`, `code_execution_20260521` | | Claude Sonnet 4.6 (claude-sonnet-4-6) | `code_execution_20250825`, `code_execution_20260120`, `code_execution_20260521` | | Claude Opus 4.5 (claude-opus-4-5-20251101) | `code_execution_20250825`, `code_execution_20260120`, `code_execution_20260521` | | Claude Sonnet 4.5 (claude-sonnet-4-5-20250929) | `code_execution_20250825`, `code_execution_20260120`, `code_execution_20260521` | | Claude Haiku 4.5 (claude-haiku-4-5-20251001) | `code_execution_20250825`, `code_execution_20260120`, `code_execution_20260521` | Each tool version builds on the previous one: * `code_execution_20250825` supports Bash commands and file operations. * `code_execution_20260120` adds REPL state persistence and [programmatic tool calling](https://platform.claude.com/docs/en/agents-and-tools/tool-use/programmatic-tool-calling) from within the sandbox. Claude Haiku 4.5 accepts the `code_execution_20260120` and `code_execution_20260521` tool types, but programmatic tool calling and the REPL state persistence that depends on it aren't available on it, so the newer versions behave like `code_execution_20250825` there. * `code_execution_20260521` is the same runtime as `code_execution_20260120`. The difference is that the tool description tells Claude about the 90-second wall-clock limit on each Python cell in programmatic tool calling, so Claude can budget long-running cells. A cell that exceeds the limit returns a normal code execution result with a non-zero `return_code` and a `detection_timeout` status message in its output. This is separate from the `execution_time_exceeded` [error code](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool#errors), which the API returns when a whole tool invocation exceeds the maximum execution time. All three tool versions are generally available and don't require an `anthropic-beta` header. The legacy code execution beta headers remain valid opt-ins. The examples on this page use `code_execution_20250825`, which covers the Bash and file operations they demonstrate and behaves the same way on every model in the table; use `code_execution_20260120` or later when you need programmatic tool calling or REPL state persistence. The current [web search](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-search-tool) and [web fetch](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-fetch-tool) tools (`web_search_20260209`, `web_fetch_20260209`, and later) require `code_execution_20260120` or later as their code execution version. If you're still using the legacy `code_execution_20250522` (Python only), see [Upgrade to latest tool version](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool#upgrade-to-latest-tool-version) to migrate from it. Older tool versions are not guaranteed to stay compatible with newer models. When you adopt a new model, check the [model compatibility table](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool#model-compatibility) and prefer the newest tool version your integration supports. ## Platform availability Code execution is available on: * **Claude API** (Anthropic) * **[Claude Platform on AWS](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws)** * **[Microsoft Foundry](https://platform.claude.com/docs/en/build-with-claude/claude-in-microsoft-foundry)** (requires a [Hosted on Anthropic deployment](https://platform.claude.com/docs/en/build-with-claude/claude-in-microsoft-foundry#additional-features-not-supported-when-hosted-on-azure)) Code execution is not currently available on Amazon Bedrock or Google Cloud. For [Claude Mythos Preview](https://anthropic.com/glasswing), code execution is supported on the Claude API and Microsoft Foundry only. It is not available for Mythos Preview on Amazon Bedrock, Claude Platform on AWS, or Google Cloud. ## Quick start Here's an example that asks Claude to perform a calculation: ```bash cURL curl --fail-with-body -sS https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 4096, "messages": [ { "role": "user", "content": "Use the code execution tool to calculate the mean and standard deviation of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]" } ], "tools": [ { "type": "code_execution_20250825", "name": "code_execution" } ] }' ``` ```bash CLI ant messages create \ --model claude-opus-5 \ --max-tokens 4096 \ --message '{ role: user, content: "Use the code execution tool to calculate the mean and standard deviation of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]" }' \ --tool '{type: code_execution_20250825, name: code_execution}' ``` ```python Python client = anthropic.Anthropic() response = client.messages.create( model="claude-opus-5", max_tokens=4096, messages=[ { "role": "user", "content": "Use the code execution tool to calculate the mean and standard deviation of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]", } ], tools=[{"type": "code_execution_20250825", "name": "code_execution"}], ) print(response.to_json()) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 4096, messages: [ { role: "user", content: "Use the code execution tool to calculate the mean and standard deviation of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]" } ], tools: [{ type: "code_execution_20250825", name: "code_execution" }] }); console.log(JSON.stringify(response)); ``` ```csharp C# AnthropicClient client = new(); var message = await client.Messages.Create(new() { Model = Model.ClaudeOpus5, MaxTokens = 4096, Messages = [new() { Role = Role.User, Content = "Use the code execution tool to calculate the mean and standard deviation of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]" }], Tools = [new CodeExecutionTool20250825()] }); Console.WriteLine(message); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.Background(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 4096, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Use the code execution tool to calculate the mean and standard deviation of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]")), }, Tools: []anthropic.ToolUnionParam{ {OfCodeExecutionTool20250825: &anthropic.CodeExecutionTool20250825Param{}}, }, }) if err != nil { log.Fatal(err) } fmt.Println(response.RawJSON()) ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(4096L) .addUserMessage("Use the code execution tool to calculate the mean and standard deviation of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]") .addTool(CodeExecutionTool20250825.builder().build()) .build(); Message response = client.messages().create(params); IO.println(ObjectMappers.jsonMapper().valueToTree(response)); ``` ```php PHP $client = new Client(); $message = $client->messages->create( maxTokens: 4096, messages: [ [ 'role' => 'user', 'content' => 'Use the code execution tool to calculate the mean and standard deviation of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]', ], ], model: Model::CLAUDE_OPUS_5, tools: [new CodeExecutionTool20250825()], ); echo json_encode($message, JSON_PRETTY_PRINT), PHP_EOL; ``` ```ruby Ruby client = Anthropic::Client.new message = client.messages.create( model: Anthropic::Model::CLAUDE_OPUS_5, max_tokens: 4096, messages: [ { role: "user", content: "Use the code execution tool to calculate the mean and standard deviation of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]" } ], tools: [Anthropic::CodeExecutionTool20250825.new] ) puts message.to_json ``` The response interleaves `server_tool_use` blocks (the commands Claude ran) with their tool result blocks, followed by Claude's text. The top level also includes a `container` object whose `id` you can [reuse across requests](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool#container-reuse). See [Response format](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool#response-format) for the block shapes. ## How code execution works When you add the code execution tool to your API request: 1. Claude evaluates whether code execution would help answer your question 2. The tool automatically provides Claude with the following capabilities: * **Bash commands:** Run shell commands for system operations * **File operations:** Create, view, and edit files directly, including writing code 3. Claude can use any combination of these capabilities in a single request 4. All operations run in a secure, sandboxed container. The container has no internet access, so Claude can't download packages at runtime: only the [pre-installed libraries](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool#pre-installed-libraries) are available 5. The API runs every command server-side and returns the results to Claude within the same request, so you never execute code or send back `tool_result` blocks yourself. One exception is when Claude calls one of your client tools alongside code execution: the API returns the code execution call without its result. The result arrives in a later response, after you send back the `tool_result` blocks for your client tools 6. Each request runs in a new container unless you pass an earlier response's container ID back (see [Container reuse](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool#container-reuse)) 7. Claude provides results with any generated charts, calculations, or analysis The container has Python pre-installed. Claude writes Python with the file operations sub-tool and runs it with a Bash command. With `code_execution_20260120` or later and [programmatic tool calling](https://platform.claude.com/docs/en/agents-and-tools/tool-use/programmatic-tool-calling), the Python interpreter state (such as variable bindings) also persists across requests that reuse the container. ### When Claude runs code Claude runs code when the request benefits from computation or file handling: * Non-trivial math (large numbers, many steps, precision-sensitive results) * Data analysis, file parsing, or visualization * Algorithm execution or simulation * Explicit requests to "run", "compute", or "execute" Claude answers directly without running code for: * Simple arithmetic and well-known math facts * Factual, conversational, or creative requests * Simple unit conversions or translations If you want Claude to run code for a borderline request, ask explicitly (for example, "run code to verify this"). ## Work with files ### Upload and analyze your own files To analyze your own data files (such as CSV, Excel, or images), upload them through the Files API and reference them in your request: Using the Files API with code execution requires the Files API beta header: `"anthropic-beta": "files-api-2025-04-14"` The Python environment can process various file types uploaded through the Files API, including: * CSV * Excel (.xlsx, .xls) * JSON * XML * Images (JPEG, PNG, GIF, WebP) * Text files (.txt, .md, .py, and others) #### Upload and analyze files 1. **Upload your file** using the [Files API](https://platform.claude.com/docs/en/build-with-claude/files) 2. **Reference the file** in your message using a `container_upload` content block 3. **Include the code execution tool** in your API request ```bash cURL # First, upload a file and capture the file ID (using jq) FILE_ID=$(curl --fail-with-body -sS https://api.anthropic.com/v1/files \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: files-api-2025-04-14" \ -F "file=@data.csv" | jq -r '.id') # Then use the file_id with code execution curl --fail-with-body -sS https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: files-api-2025-04-14" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 4096, "messages": [{ "role": "user", "content": [ {"type": "text", "text": "Analyze this CSV data"}, {"type": "container_upload", "file_id": "'"$FILE_ID"'"} ] }], "tools": [{ "type": "code_execution_20250825", "name": "code_execution" }] }' ``` ```bash CLI # First, upload a file and capture the file ID FILE_ID=$(ant beta:files upload \ --file ./data.csv \ --transform id --raw-output) # Then use the file_id with code execution ant beta:messages create \ --beta files-api-2025-04-14 <beta->files->upload( file: FileParam::fromResource(fopen('data.csv', 'r')), ); // Use the file_id with code execution $response = $client->beta->messages->create( model: Model::CLAUDE_OPUS_5, maxTokens: 4096, betas: [AnthropicBeta::FILES_API_2025_04_14], messages: [ [ 'role' => 'user', 'content' => [ BetaTextBlockParam::with(text: 'Analyze this CSV data'), BetaContainerUploadBlockParam::with(fileID: $fileObject->id), ], ], ], tools: [new BetaCodeExecutionTool20250825()], ); echo json_encode($response), PHP_EOL; ``` ```ruby Ruby client = Anthropic::Client.new # Upload a file file_object = client.beta.files.upload( file: Pathname("data.csv") ) # Use the file_id with code execution response = client.beta.messages.create( model: Anthropic::Model::CLAUDE_OPUS_5, betas: [Anthropic::AnthropicBeta::FILES_API_2025_04_14], max_tokens: 4096, messages: [ { role: "user", content: [ { type: "text", text: "Analyze this CSV data" }, { type: "container_upload", file_id: file_object.id } ] } ], tools: [ Anthropic::Beta::BetaCodeExecutionTool20250825.new ] ) puts response.to_json ``` ### Retrieve generated files When Claude saves files to its output directory during code execution (see [How generated files are captured](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool#how-generated-files-are-captured)), each file's ID appears in the code execution tool result, and you can download it with the [Files API](https://platform.claude.com/docs/en/build-with-claude/files): ```bash cURL # Downloading every generated file means looping over the file IDs in the tool # result, which doesn't translate to a one-off shell command. Use one of the # SDK examples instead. ``` ```bash CLI # Extracting every file ID from the tool results and downloading each one # requires a loop, which doesn't translate well to a one-off CLI command. # Use one of the SDK examples instead. ``` ```python Python client = Anthropic() # Request code execution that creates files response = client.beta.messages.create( model="claude-opus-5", betas=["files-api-2025-04-14"], max_tokens=4096, messages=[ { "role": "user", "content": "Create a matplotlib visualization and save it as output.png", } ], tools=[{"type": "code_execution_20250825", "name": "code_execution"}], ) # Extract file IDs from the response def extract_file_ids(response: BetaMessage) -> list[str]: file_ids: list[str] = [] for item in response.content: if item.type == "bash_code_execution_tool_result": content_item = item.content if content_item.type == "bash_code_execution_result": for output_block in content_item.content: file_ids.append(output_block.file_id) return file_ids # Download the created files for file_id in extract_file_ids(response): file_metadata = client.beta.files.retrieve_metadata(file_id) file_content = client.beta.files.download(file_id) file_content.write_to_file(file_metadata.filename) print(f"Downloaded: {file_metadata.filename}") ``` ```typescript TypeScript import { writeFile } from "node:fs/promises"; const client = new Anthropic(); // Request code execution that creates files const response = await client.beta.messages.create({ model: "claude-opus-5", betas: ["files-api-2025-04-14"], max_tokens: 4096, messages: [ { role: "user", content: "Create a matplotlib visualization and save it as output.png" } ], tools: [ { type: "code_execution_20250825", name: "code_execution" } ] }); // Extract the file IDs from the response and download each created file for (const block of response.content) { if (block.type === "bash_code_execution_tool_result") { const result = block.content; if (result.type === "bash_code_execution_result") { for (const outputBlock of result.content) { const [fileMetadata, fileResponse] = await Promise.all([ client.beta.files.retrieveMetadata(outputBlock.file_id), client.beta.files.download(outputBlock.file_id) ]); await writeFile(fileMetadata.filename, await fileResponse.bytes()); console.log(`Downloaded: ${fileMetadata.filename}`); } } } } ``` ```csharp C# AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 4096, Betas = [AnthropicBeta.FilesApi2025_04_14], Messages = [new() { Role = Role.User, Content = "Create a matplotlib visualization and save it as output.png" }], Tools = [new BetaCodeExecutionTool20250825()] }; var response = await client.Beta.Messages.Create(parameters); // Collect the file IDs from the tool results List fileIds = []; foreach (var block in response.Content) { if (!block.TryPickBashCodeExecutionToolResult(out var toolResult)) continue; if (!toolResult.Content.TryPickBetaBashCodeExecutionResultBlock(out var result)) continue; foreach (var output in result.Content) { fileIds.Add(output.FileID); } } // Download each created file foreach (var fileId in fileIds) { var fileMetadata = await client.Beta.Files.RetrieveMetadata(fileId); using var download = await client.Beta.Files.Download(fileId); var downloadStream = await download.ReadAsStream(); await using var target = File.Create(fileMetadata.Filename); await downloadStream.CopyToAsync(target); Console.WriteLine($"Downloaded: {fileMetadata.Filename}"); } ``` ```go Go client := anthropic.NewClient() ctx := context.Background() response, err := client.Beta.Messages.New(ctx, anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 4096, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Create a matplotlib visualization and save it as output.png")), }, Tools: []anthropic.BetaToolUnionParam{ {OfCodeExecutionTool20250825: &anthropic.BetaCodeExecutionTool20250825Param{}}, }, Betas: []anthropic.AnthropicBeta{ anthropic.AnthropicBetaFilesAPI2025_04_14, }, }) if err != nil { log.Fatal(err) } fileIDs := extractFileIDs(response) for _, fileID := range fileIDs { fileMetadata, err := client.Beta.Files.GetMetadata(ctx, fileID, anthropic.BetaFileGetMetadataParams{}) if err != nil { log.Fatal(err) } fileContent, err := client.Beta.Files.Download(ctx, fileID, anthropic.BetaFileDownloadParams{}) if err != nil { log.Fatal(err) } outFile, err := os.Create(fileMetadata.Filename) if err != nil { log.Fatal(err) } _, err = io.Copy(outFile, fileContent.Body) if err != nil { log.Fatal(err) } outFile.Close() fileContent.Body.Close() fmt.Printf("Downloaded: %s\n", fileMetadata.Filename) } // ... func extractFileIDs(response *anthropic.BetaMessage) []string { var fileIDs []string for _, item := range response.Content { switch variant := item.AsAny().(type) { case anthropic.BetaBashCodeExecutionToolResultBlock: // Collect the file IDs from the tool result for _, file := range variant.Content.Content { if file.FileID != "" { fileIDs = append(fileIDs, file.FileID) } } } } return fileIDs } ``` ```java Java void main() throws Exception { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .addBeta(AnthropicBeta.FILES_API_2025_04_14) .maxTokens(4096L) .addUserMessage("Create a matplotlib visualization and save it as output.png") .addTool(BetaCodeExecutionTool20250825.builder().build()) .build(); BetaMessage response = client.beta().messages().create(params); List fileIds = extractFileIds(response); for (String fileId : fileIds) { FileMetadata fileMetadata = client.beta().files().retrieveMetadata(fileId); try (HttpResponse fileContent = client.beta().files().download(fileId)) { Files.copy( fileContent.body(), Path.of(fileMetadata.filename()), StandardCopyOption.REPLACE_EXISTING); } IO.println("Downloaded: " + fileMetadata.filename()); } } List extractFileIds(BetaMessage response) { List fileIds = new ArrayList<>(); // Collect the file IDs from the tool results for (BetaContentBlock item : response.content()) { item.bashCodeExecutionToolResult().ifPresent(toolResult -> { if (toolResult.content().isBetaBashCodeExecutionResultBlock()) { BetaBashCodeExecutionResultBlock result = toolResult.content().asBetaBashCodeExecutionResultBlock(); for (BetaBashCodeExecutionOutputBlock output : result.content()) { fileIds.add(output.fileId()); } } }); } return fileIds; } ``` ```php PHP $client = new Client(); // Request code execution that creates files $response = $client->beta->messages->create( maxTokens: 4096, messages: [ [ 'role' => 'user', 'content' => 'Create a matplotlib visualization and save it as output.png', ], ], model: Model::CLAUDE_OPUS_5, betas: [AnthropicBeta::FILES_API_2025_04_14], tools: [new BetaCodeExecutionTool20250825()], ); /** * Extract file IDs from the response. * * @return list */ function extractFileIds(BetaMessage $response): array { $fileIds = []; foreach ($response->content as $block) { if ($block->type !== 'bash_code_execution_tool_result') { continue; } $resultBlock = $block->content; if ($resultBlock->type !== 'bash_code_execution_result') { continue; } foreach ($resultBlock->content as $outputBlock) { $fileIds[] = $outputBlock->fileID; } } return $fileIds; } // Download the created files foreach (extractFileIds($response) as $fileId) { $fileMetadata = $client->beta->files->retrieveMetadata($fileId); $fileContent = $client->beta->files->download($fileId); file_put_contents($fileMetadata->filename, $fileContent); echo "Downloaded: {$fileMetadata->filename}\n"; } ``` ```ruby Ruby client = Anthropic::Client.new response = client.beta.messages.create( model: Anthropic::Model::CLAUDE_OPUS_5, betas: ["files-api-2025-04-14"], max_tokens: 4096, messages: [ { role: "user", content: "Create a matplotlib visualization and save it as output.png" } ], tools: [ { type: "code_execution_20250825", name: "code_execution" } ] ) def extract_file_ids(response) file_ids = [] response.content.each do |item| if item.type == :bash_code_execution_tool_result # WORKAROUND for anthropic-sdk-ruby union coercion bug (SDK-636): item.content is a # nested content union, so the typed accessors on `item.content` are unreliable. # Read the raw response data through the public `BaseModel#[]` API instead. content_item = item.content if content_item[:type].to_s == "bash_code_execution_result" Array(content_item[:content]).each do |output_block| file_ids << output_block[:file_id] end end end end file_ids end extract_file_ids(response).each do |file_id| file_metadata = client.beta.files.retrieve_metadata(file_id) file_content = client.beta.files.download(file_id) File.open(file_metadata.filename, "wb") do |f| f.write(file_content.read) end puts "Downloaded: #{file_metadata.filename}" end ``` #### How generated files are captured Each `bash_code_execution` call gets a new, empty directory, available to the command as `$OUTPUT_DIR`. When the command finishes, the files at the top level of that directory are captured and returned as the `file_id` entries in the result's `content` list. Files written anywhere else stay in the container and aren't returned. The tool description tells Claude to share files by copying them into `$OUTPUT_DIR`. If your application depends on receiving a file, prompt Claude to copy it into `$OUTPUT_DIR` and list the directory in the same command, so the `ls` output confirms the capture (Claude doesn't see the `content` list): ```bash python /tmp/make_report.py && cp /tmp/report.pdf "$OUTPUT_DIR/" && ls "$OUTPUT_DIR" ``` A file Claude wrote elsewhere is still in the container, so you can [reuse the container](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool#container-reuse) and ask Claude to copy it into `$OUTPUT_DIR`. ## Tool definition The code execution tool requires no additional parameters: ```json JSON { "type": "code_execution_20250825", "name": "code_execution" } ``` Both fields are fixed: `type` selects the tool version, and `name` must be `code_execution`. When you provide this tool, Claude automatically gains access to two sub-tools: * `bash_code_execution`: Run shell commands * `text_editor_code_execution`: View, create, and edit files, including writing code When Claude runs code, the response also includes a top-level `container` object with the container's `id` and `expires_at` timestamp. Pass that ID back in the top-level `container` request parameter to keep using the same container. See [Container reuse](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool#container-reuse). ## Response format The code execution tool can return two types of results depending on the operation: ### Bash command response ```json Output { "type": "server_tool_use", "id": "srvtoolu_01B3C4D5E6F7G8H9I0J1K2L3", "name": "bash_code_execution", "input": { "command": "ls -la | head -5" } }, { "type": "bash_code_execution_tool_result", "tool_use_id": "srvtoolu_01B3C4D5E6F7G8H9I0J1K2L3", "content": { "type": "bash_code_execution_result", "stdout": "total 24\ndrwxr-xr-x 2 user user 4096 Jan 1 12:00 .\ndrwxr-xr-x 3 user user 4096 Jan 1 11:00 ..\n-rw-r--r-- 1 user user 220 Jan 1 12:00 data.csv\n-rw-r--r-- 1 user user 180 Jan 1 12:00 config.json", "stderr": "", "return_code": 0, "content": [] } } ``` ### File operation responses **View file:** ```json Output { "type": "server_tool_use", "id": "srvtoolu_01C4D5E6F7G8H9I0J1K2L3M4", "name": "text_editor_code_execution", "input": { "command": "view", "path": "config.json" } }, { "type": "text_editor_code_execution_tool_result", "tool_use_id": "srvtoolu_01C4D5E6F7G8H9I0J1K2L3M4", "content": { "type": "text_editor_code_execution_view_result", "file_type": "text", "content": "{\n \"setting\": \"value\",\n \"debug\": true\n}", "num_lines": 4, "start_line": 1, "total_lines": 4 } } ``` **Create file:** ```json Output { "type": "server_tool_use", "id": "srvtoolu_01D5E6F7G8H9I0J1K2L3M4N5", "name": "text_editor_code_execution", "input": { "command": "create", "path": "new_file.txt", "file_text": "Hello, World!" } }, { "type": "text_editor_code_execution_tool_result", "tool_use_id": "srvtoolu_01D5E6F7G8H9I0J1K2L3M4N5", "content": { "type": "text_editor_code_execution_create_result", "is_file_update": false } } ``` **Edit file (str\_replace):** ```json Output { "type": "server_tool_use", "id": "srvtoolu_01E6F7G8H9I0J1K2L3M4N5O6", "name": "text_editor_code_execution", "input": { "command": "str_replace", "path": "config.json", "old_str": "\"debug\": true", "new_str": "\"debug\": false" } }, { "type": "text_editor_code_execution_tool_result", "tool_use_id": "srvtoolu_01E6F7G8H9I0J1K2L3M4N5O6", "content": { "type": "text_editor_code_execution_str_replace_result", "old_start": 3, "old_lines": 1, "new_start": 3, "new_lines": 1, "lines": ["- \"debug\": true", "+ \"debug\": false"] } } ``` ### Results Bash command results (`bash_code_execution_result`) include: * `stdout`: Output from successful execution * `stderr`: Error messages if execution fails * `return_code`: 0 for success, non-zero for failure * `content`: A list with an entry for each file the command left in `$OUTPUT_DIR` (see [How generated files are captured](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool#how-generated-files-are-captured)). Each entry carries the `file_id` to [retrieve the file](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool#retrieve-generated-files) with the Files API File operation results have their own fields: * **View** (`text_editor_code_execution_view_result`): `file_type`, `content`, `num_lines`, `start_line`, `total_lines` * **Create** (`text_editor_code_execution_create_result`): `is_file_update` (whether the file already existed) * **Edit** (`text_editor_code_execution_str_replace_result`): `old_start`, `old_lines`, `new_start`, `new_lines`, `lines` (diff format) ### Errors Each tool type can return specific errors: **Common errors (all tools):** ```json Output { "type": "bash_code_execution_tool_result", "tool_use_id": "srvtoolu_01VfmxgZ46TiHbmXgy928hQR", "content": { "type": "bash_code_execution_tool_result_error", "error_code": "unavailable" } } ``` **Error codes by tool type:** | Tool | Error code | Description | | ------------ | ------------------------- | ------------------------------------------------------- | | All tools | `unavailable` | The tool is temporarily unavailable | | All tools | `execution_time_exceeded` | The tool invocation exceeded the maximum execution time | | All tools | `invalid_tool_input` | Invalid parameters provided to the tool | | All tools | `too_many_requests` | Rate limit exceeded for tool usage | | bash | `output_file_too_large` | Command output exceeded the maximum size | | text\_editor | `file_not_found` | File doesn't exist (for view/edit operations) | An expired container can't be reused: requests that reference it return an error instead of restoring it. Send the request again without the `container` parameter to get a new container. ### `pause_turn` stop reason The response might include a `pause_turn` stop reason, which indicates that the API paused a long-running turn. You may provide the response back as-is in a subsequent request to let Claude continue its turn, or modify the content if you want to interrupt the conversation. ## Containers The code execution tool runs in a secure, containerized environment designed specifically for code execution, with a higher focus on Python. ### Runtime environment * **Python version:** 3.11 * **Operating system:** Linux-based container * **Architecture:** x86\_64 (AMD64) ### Resource limits * **Memory:** 5 GiB RAM * **Disk space:** 5 GiB workspace storage * **CPU:** 1 CPU * **Execution time:** A tool invocation that runs past the maximum execution time returns an `execution_time_exceeded` [error](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool#errors). With [programmatic tool calling](https://platform.claude.com/docs/en/agents-and-tools/tool-use/programmatic-tool-calling), each REPL cell also has a 90-second wall-clock limit ### Networking and security * **Internet access:** Completely disabled for security * **External connections:** No outbound network requests permitted * **Sandbox isolation:** Full isolation from host system and other containers * **File access:** Limited to workspace directory only * **Workspace scoping:** Like the [Files API](https://platform.claude.com/docs/en/build-with-claude/files), containers are scoped to the workspace of the API key * **Expiration:** Containers expire 30 days after creation ### Pre-installed libraries The sandboxed Python environment includes these commonly used libraries: * **Data science:** pandas, numpy, scipy, scikit-learn, statsmodels * **Visualization:** matplotlib, seaborn * **File processing:** pyarrow, openpyxl, xlsxwriter, xlrd, pillow, python-pptx, python-docx, pypdf, pdfplumber, pypdfium2, pdf2image, pdfkit, tabula-py, reportlab\[pycairo], Img2pdf * **Math and computing:** sympy, mpmath * **Utilities:** tqdm, python-dateutil, pytz, joblib The container also includes command-line tools such as unzip, unrar, 7zip, bc, rg (ripgrep), fd, and sqlite. The container has no internet access, so Claude can't download or install additional packages at runtime: only the pre-installed libraries are available. ## Container reuse You can reuse an existing container across multiple API requests by providing the container ID from a previous response. This allows you to maintain created files between requests. With `code_execution_20260120` or later and [programmatic tool calling](https://platform.claude.com/docs/en/agents-and-tools/tool-use/programmatic-tool-calling), the Python interpreter state persists as well. Containers expire 30 days after creation. After about 5 minutes of inactivity a container is checkpointed, and sending a request with its ID inside the 30-day window restores it. The `expires_at` timestamp in the response's `container` object is a shorter rolling value and doesn't report the 30-day limit. A container that has expired can't be reused. Send the request again without the `container` parameter to get a new container. ### Example ```bash cURL # First request: Create a file with a random number, capturing the container ID (using jq) CONTAINER_ID=$(curl -s https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 4096, "messages": [{ "role": "user", "content": "Write a file with a random number and save it to \"/tmp/number.txt\"" }], "tools": [{ "type": "code_execution_20250825", "name": "code_execution" }] }' | jq -r '.container.id') # Second request: Reuse the container to read the file curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "container": "'"$CONTAINER_ID"'", "model": "claude-opus-5", "max_tokens": 4096, "messages": [{ "role": "user", "content": "Read the number from \"/tmp/number.txt\" and calculate its square" }], "tools": [{ "type": "code_execution_20250825", "name": "code_execution" }] }' ``` ```bash CLI # First request: Create a file with a random number CONTAINER_ID=$(ant messages create \ --model claude-opus-5 \ --max-tokens 4096 \ --message '{role: user, content: Write a file with a random number and save it to "/tmp/number.txt"}' \ --tool '{type: code_execution_20250825, name: code_execution}' \ --transform container.id --raw-output) # Second request: Reuse the container to read the file ant messages create \ --container "$CONTAINER_ID" \ --model claude-opus-5 \ --max-tokens 4096 \ --message '{role: user, content: Read the number from "/tmp/number.txt" and calculate its square}' \ --tool '{type: code_execution_20250825, name: code_execution}' ``` ```python Python client = anthropic.Anthropic() # First request: create a file with a random number in a new container response1 = client.messages.create( model="claude-opus-5", max_tokens=4096, messages=[ { "role": "user", "content": "Write a file with a random number and save it to '/tmp/number.txt'", } ], tools=[{"type": "code_execution_20250825", "name": "code_execution"}], ) # Second request: pass the container ID back so Claude reuses the same container response2 = client.messages.create( container=response1.container.id, model="claude-opus-5", max_tokens=4096, messages=[ { "role": "user", "content": "Read the number from '/tmp/number.txt' and calculate its square", } ], tools=[{"type": "code_execution_20250825", "name": "code_execution"}], ) print(response2.to_json()) ``` ```typescript TypeScript const client = new Anthropic(); // First request: Claude creates a file inside a fresh code execution container const response1 = await client.messages.create({ model: "claude-opus-5", max_tokens: 4096, messages: [ { role: "user", content: "Write a file with a random number and save it to '/tmp/number.txt'" } ], tools: [{ type: "code_execution_20250825", name: "code_execution" }] }); // The response includes the container once the code execution tool has run if (!response1.container) { throw new Error("Expected the first response to include a container"); } // Second request: pass the container ID back so it reuses the same container const response2 = await client.messages.create({ container: response1.container.id, model: "claude-opus-5", max_tokens: 4096, messages: [ { role: "user", content: "Read the number from /tmp/number.txt and calculate its square" } ], tools: [{ type: "code_execution_20250825", name: "code_execution" }] }); console.log(JSON.stringify(response2)); ``` ```csharp C# AnthropicClient client = new(); // First request: Claude creates a file inside a fresh code execution container var response1 = await client.Messages.Create(new() { Model = Model.ClaudeOpus5, MaxTokens = 4096, Messages = [new() { Role = Role.User, Content = "Write a file with a random number and save it to '/tmp/number.txt'" }], Tools = [new CodeExecutionTool20250825()] }); // Second request: pass the container ID back so Claude reuses the same container var response2 = await client.Messages.Create(new() { Container = response1.Container!.ID, Model = Model.ClaudeOpus5, MaxTokens = 4096, Messages = [new() { Role = Role.User, Content = "Read the number from '/tmp/number.txt' and calculate its square" }], Tools = [new CodeExecutionTool20250825()] }); Console.WriteLine(response2); ``` ```go Go client := anthropic.NewClient() ctx := context.Background() codeExecution := []anthropic.ToolUnionParam{ {OfCodeExecutionTool20250825: &anthropic.CodeExecutionTool20250825Param{}}, } // First request: create a file with a random number in a new container response1, err := client.Messages.New(ctx, anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 4096, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Write a file with a random number and save it to '/tmp/number.txt'")), }, Tools: codeExecution, }) if err != nil { log.Fatal(err) } // Reuse the container from the first request so the file is still there. response2, err := client.Messages.New(ctx, anthropic.MessageNewParams{ Container: anthropic.String(response1.Container.ID), Model: anthropic.ModelClaudeOpus5, MaxTokens: 4096, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Read the number from '/tmp/number.txt' and calculate its square")), }, Tools: codeExecution, }) if err != nil { log.Fatal(err) } fmt.Println(response2.RawJSON()) ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); // First request: create a file with a random number in a new container MessageCreateParams params1 = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(4096L) .addUserMessage("Write a file with a random number and save it to '/tmp/number.txt'") .addTool(CodeExecutionTool20250825.builder().build()) .build(); Message response1 = client.messages().create(params1); // Second request: pass the container ID back so it reuses the same container MessageCreateParams params2 = MessageCreateParams.builder() .container(response1.container().orElseThrow().id()) .model(Model.CLAUDE_OPUS_5) .maxTokens(4096L) .addUserMessage("Read the number from '/tmp/number.txt' and calculate its square") .addTool(CodeExecutionTool20250825.builder().build()) .build(); Message response2 = client.messages().create(params2); IO.println(ObjectMappers.jsonMapper().valueToTree(response2)); ``` ```php PHP $client = new Client(); // First request: Claude writes the file inside a fresh code execution container $response1 = $client->messages->create( maxTokens: 4096, messages: [ [ 'role' => 'user', 'content' => "Write a file with a random number and save it to '/tmp/number.txt'", ], ], model: Model::CLAUDE_OPUS_5, tools: [new CodeExecutionTool20250825()], ); // Second request: reuse the container so '/tmp/number.txt' is still there $response2 = $client->messages->create( container: $response1->container->id, maxTokens: 4096, messages: [ [ 'role' => 'user', 'content' => "Read the number from '/tmp/number.txt' and calculate its square", ], ], model: Model::CLAUDE_OPUS_5, tools: [new CodeExecutionTool20250825()], ); echo json_encode($response2), PHP_EOL; ``` ```ruby Ruby client = Anthropic::Client.new # First request: Claude creates the file inside a fresh code execution container response1 = client.messages.create( model: Anthropic::Model::CLAUDE_OPUS_5, max_tokens: 4096, messages: [ { role: "user", content: "Write a file with a random number and save it to '/tmp/number.txt'" } ], tools: [Anthropic::CodeExecutionTool20250825.new] ) # Second request: pass the container ID back so Claude reuses the same container response2 = client.messages.create( container: response1.container.id, model: Anthropic::Model::CLAUDE_OPUS_5, max_tokens: 4096, messages: [ { role: "user", content: "Read the number from '/tmp/number.txt' and calculate its square" } ], tools: [Anthropic::CodeExecutionTool20250825.new] ) puts response2.to_json ``` ## Using code execution with other execution tools When you provide code execution alongside client-provided tools that also run code (such as a [Bash tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/bash-tool) or custom REPL), Claude is operating in a multicomputer environment. The code execution tool runs in Anthropic's sandboxed container, while your client-provided tools run in a separate environment that you control. Claude can sometimes confuse these environments, attempting to use the wrong tool or assuming state is shared between them. To avoid this, add instructions to your system prompt that clarify the distinction: ```text wrap When multiple code execution environments are available, be aware that: - Variables, files, and state do NOT persist between different execution environments - Use the code_execution tool for general-purpose computation in Anthropic's sandboxed environment - Use client-provided execution tools (e.g., bash) when you need access to the user's local system, files, or data - If you need to pass results between environments, explicitly include outputs in subsequent tool calls rather than assuming shared state ``` This is especially important when combining code execution with [web search](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-search-tool) or [web fetch](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-fetch-tool), which enable code execution automatically. If your application already provides a client-side shell tool, the automatic code execution creates a second execution environment that Claude needs to distinguish between. When Claude calls one of your client tools alongside code execution, the API returns the code execution call without its result. The result arrives in a later response, after you send back the `tool_result` blocks for your client tools. ## Streaming With [streaming](https://platform.claude.com/docs/en/build-with-claude/streaming) enabled (`"stream": true`), you'll receive code execution events as they occur. The sub-tool input streams as `input_json_delta` events, and each result block arrives whole in a single `content_block_start` event: ```sse event: content_block_start data: {"type": "content_block_start", "index": 1, "content_block": {"type": "server_tool_use", "id": "srvtoolu_xyz789", "name": "bash_code_execution"}} // Tool input streamed as partial JSON event: content_block_delta data: {"type": "content_block_delta", "index": 1, "delta": {"type": "input_json_delta", "partial_json": "{\"command\": \"python analyze.py\"}"}} // Pause while the command runs // Execution result delivered as a complete block event: content_block_start data: {"type": "content_block_start", "index": 2, "content_block": {"type": "bash_code_execution_tool_result", "tool_use_id": "srvtoolu_xyz789", "content": {"type": "bash_code_execution_result", "stdout": " A B C\n0 1 2 3\n1 4 5 6", "stderr": "", "return_code": 0, "content": []}}} ``` ## Batch requests You can include the code execution tool in the [Messages Batches API](https://platform.claude.com/docs/en/build-with-claude/batch-processing). Code execution tool calls through the Messages Batches API are priced the same as those in regular Messages API requests. ## Usage and pricing **Code execution is free when used with web search or web fetch.** When `web_search_20260209` (or later) or `web_fetch_20260209` (or later) is included in your API request, there are no additional charges for code execution tool calls beyond the standard input and output token costs. When used without these tools, code execution is billed by execution time, tracked separately from token usage: * Execution time has a minimum of 5 minutes * Each organization receives **1,550 free hours** of usage per month * Additional usage beyond 1,550 hours is billed at **$0.05 USD per hour, per container** * If files are included in the request, execution time is billed even if the tool is not called, because files are preloaded onto the container Code execution usage is tracked in the response: ```json { "usage": { "input_tokens": 105, "output_tokens": 239, "server_tool_use": { "code_execution_requests": 1 } } } ``` ## Upgrade to latest tool version The latest tool version is `code_execution_20260521`. To move between the three current versions, update the `type` string in your request: all three return the response blocks documented in [Response format](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool#response-format). See [Model compatibility](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool#model-compatibility) for what each version adds and which models support it. The rest of this section covers migrating from the legacy Python-only `code_execution_20250522` to the current tool versions. ### What's changed | Component | Legacy | Current | | -------------- | --------------------------- | ------------------------------------------------------------------- | | Beta header | `code-execution-2025-05-22` | None required | | Tool type | `code_execution_20250522` | `code_execution_20250825` or later | | Capabilities | Python only | Bash commands, file operations | | Response types | `code_execution_result` | `bash_code_execution_result`, `text_editor_code_execution_*_result` | ### Backward compatibility * All existing Python code execution continues to work exactly as before * No changes required to existing Python-only workflows ### Upgrade steps To upgrade, update the tool type in your API requests: ```diff - "type": "code_execution_20250522" + "type": "code_execution_20250825" ``` **Review response handling** (if parsing responses programmatically): * The API no longer sends the previous blocks for Python execution responses * Instead, the API sends new response types for Bash and file operations (see [Response format](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool#response-format)) ## Data retention Code execution runs in server-side sandbox containers. Container data, including execution artifacts, uploaded files, and outputs, is retained for up to 30 days. This retention applies to all data processed within the container environment. Files that code execution creates in the [Files API](https://platform.claude.com/docs/en/build-with-claude/files) (retrievable with `client.beta.files.download()`) persist until explicitly deleted. For ZDR eligibility across all features, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention). ## Next steps Pair a faster executor model with a higher-intelligence advisor model that provides strategic guidance mid-generation. Call your own tools from code that runs inside the code execution container. Upload files for analysis and download the files that code execution creates. Learn how to use Agent Skills to extend Claude's capabilities through the API. --- title: Computer use tool url: https://platform.claude.com/docs/en/agents-and-tools/tool-use/computer-use-tool description: Give Claude screenshot, mouse, and keyboard control of a desktop environment with the computer use tool. --- ## Compatibility - Status: Beta - [Beta header](https://platform.claude.com/docs/en/api/beta-headers): `computer-use-2025-11-24` - [ZDR](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention): eligible (excludes [Covered Models](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#model-specific-data-retention-requirements)) - Supported models: `claude-opus-5`, `claude-sonnet-5`, `claude-opus-4-8`, `claude-opus-4-7`, `claude-opus-4-6`, `claude-sonnet-4-6`, `claude-opus-4-5-20251101` - Platforms: Claude API (beta), Claude Platform on AWS (beta), Amazon Bedrock (beta), Google Cloud (beta), Microsoft Foundry (beta) Claude can interact with computer environments through the computer use tool, which provides screenshot capabilities and mouse/keyboard control for autonomous desktop interaction. On Claude Sonnet 4.5, Claude Haiku 4.5, Claude Opus 4.1 ([retired, except on Bedrock and Google Cloud](https://platform.claude.com/docs/en/about-claude/model-deprecations)), Claude Sonnet 4 ([retired, except on Bedrock and Google Cloud](https://platform.claude.com/docs/en/about-claude/model-deprecations)), and Claude Opus 4 ([retired, except on Google Cloud](https://platform.claude.com/docs/en/about-claude/model-deprecations)), use the earlier `computer-use-2025-01-24` [beta header](https://platform.claude.com/docs/en/api/beta-headers) instead of `computer-use-2025-11-24`. Reach out through the [feedback form](https://forms.gle/H6UFuXaaLywri9hz6) to share your feedback on this feature. ## Overview Computer use is a beta feature that enables Claude to interact with desktop environments. This tool provides: * **Screenshot capture:** See what's currently displayed on screen * **Mouse control:** Click, drag, and move the cursor * **Keyboard input:** Type text and use keyboard shortcuts * **Desktop automation:** Interact with any application or interface While computer use can be augmented with other tools such as bash and text editor for more comprehensive automation workflows, computer use specifically refers to the computer use tool's capability to see and control desktop environments. For model support, see the [Tool reference](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-reference). ## Security considerations Computer use is a beta feature with unique risks distinct from standard API features. These risks are heightened when interacting with the internet. To minimize risks, consider taking precautions such as: 1. Using a dedicated virtual machine or container with minimal privileges to prevent direct system attacks or accidents. 2. Avoiding giving the model access to sensitive data, such as account login information, to prevent information theft. 3. Limiting internet access to an allowlist of domains to reduce exposure to malicious content. 4. Asking a human to confirm decisions that might result in meaningful real-world consequences and any tasks requiring affirmative consent, such as accepting cookies, completing financial transactions, or agreeing to terms of service. In some circumstances, Claude will follow commands found in content even when they conflict with your instructions. For example, instructions on webpages or contained in images might override your instructions or cause Claude to make mistakes. Take precautions to isolate Claude from sensitive data and actions to avoid risks related to prompt injection. Anthropic has trained the model to resist these prompt injections and has added an extra layer of defense. If you use the computer use tools, classifiers will automatically run on your prompts to flag potential instances of prompt injections. When these classifiers identify potential prompt injections in screenshots, they will automatically steer the model to ask for user confirmation before proceeding with the next action. This extra protection won't be ideal for every use case (for example, use cases without a human in the loop), so if you'd like to opt out and turn it off, [contact support](https://support.claude.com/en/). These precautions remain important even with the classifier defense layer in place. Inform end users of relevant risks and obtain their consent prior to enabling computer use in your own products. Get started with the computer use reference implementation that includes a web interface, Docker container, example tool implementations, and an agent loop. ## Quick start Here's how to get started with computer use: ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: computer-use-2025-11-24" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "tools": [ { "type": "computer_20251124", "name": "computer", "display_width_px": 1024, "display_height_px": 768, "display_number": 1 }, { "type": "text_editor_20250728", "name": "str_replace_based_edit_tool" }, { "type": "bash_20250124", "name": "bash" } ], "messages": [ { "role": "user", "content": "Save a picture of a cat to my desktop." } ] }' ``` ```bash CLI ant beta:messages create --beta computer-use-2025-11-24 <<'YAML' model: claude-opus-5 max_tokens: 1024 tools: - type: computer_20251124 name: computer display_width_px: 1024 display_height_px: 768 display_number: 1 - type: text_editor_20250728 name: str_replace_based_edit_tool - type: bash_20250124 name: bash messages: - role: user content: Save a picture of a cat to my desktop. YAML ``` ```python Python client = anthropic.Anthropic() response = client.beta.messages.create( model="claude-opus-5", # or another compatible model max_tokens=1024, tools=[ { "type": "computer_20251124", "name": "computer", "display_width_px": 1024, "display_height_px": 768, "display_number": 1, }, {"type": "text_editor_20250728", "name": "str_replace_based_edit_tool"}, {"type": "bash_20250124", "name": "bash"}, ], messages=[{"role": "user", "content": "Save a picture of a cat to my desktop."}], betas=["computer-use-2025-11-24"], ) print(response) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.beta.messages.create({ model: "claude-opus-5", max_tokens: 1024, tools: [ { type: "computer_20251124", name: "computer", display_width_px: 1024, display_height_px: 768, display_number: 1 }, { type: "text_editor_20250728", name: "str_replace_based_edit_tool" }, { type: "bash_20250124", name: "bash" } ], messages: [{ role: "user", content: "Save a picture of a cat to my desktop." }], betas: ["computer-use-2025-11-24"] }); console.log(response); ``` ```csharp C# using Anthropic.Models.Beta.Messages; using Messages = Anthropic.Models.Messages; var client = new AnthropicClient(); var parameters = new MessageCreateParams { Model = Messages::Model.ClaudeOpus5, MaxTokens = 1024, Tools = new BetaToolUnion[] { new BetaToolComputerUse20251124 { DisplayWidthPx = 1024, DisplayHeightPx = 768, DisplayNumber = 1 }, new BetaToolTextEditor20250728(), new BetaToolBash20250124() }, Messages = [ new BetaMessageParam { Role = Role.User, Content = "Save a picture of a cat to my desktop." } ], Betas = ["computer-use-2025-11-24"] }; var response = await client.Beta.Messages.Create(parameters); Console.WriteLine(response); ``` ```go Go client := anthropic.NewClient() response, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Tools: []anthropic.BetaToolUnionParam{ {OfComputerUseTool20251124: &anthropic.BetaToolComputerUse20251124Param{ DisplayWidthPx: 1024, DisplayHeightPx: 768, DisplayNumber: anthropic.Int(1), }}, {OfTextEditor20250728: &anthropic.BetaToolTextEditor20250728Param{}}, {OfBashTool20250124: &anthropic.BetaToolBash20250124Param{}}, }, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Save a picture of a cat to my desktop.")), }, Betas: []anthropic.AnthropicBeta{ "computer-use-2025-11-24", // no SDK exposes a named constant for this beta yet }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java import com.anthropic.models.beta.messages.BetaMessage; import com.anthropic.models.beta.messages.BetaToolBash20250124; import com.anthropic.models.beta.messages.BetaToolComputerUse20251124; import com.anthropic.models.beta.messages.BetaToolTextEditor20250728; import com.anthropic.models.beta.messages.MessageCreateParams; import com.anthropic.models.messages.Model; void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addTool(BetaToolComputerUse20251124.builder() .displayWidthPx(1024L) .displayHeightPx(768L) .displayNumber(1L) .build()) .addTool(BetaToolTextEditor20250728.builder().build()) .addTool(BetaToolBash20250124.builder().build()) .addUserMessage("Save a picture of a cat to my desktop.") .addBeta("computer-use-2025-11-24") .build(); BetaMessage response = client.beta().messages().create(params); IO.println(response); } ``` ```php PHP $client = new Client(); $response = $client->beta->messages->create( maxTokens: 1024, messages: [ ['role' => 'user', 'content' => 'Save a picture of a cat to my desktop.'], ], model: 'claude-opus-5', tools: [ [ 'type' => 'computer_20251124', 'name' => 'computer', 'display_width_px' => 1024, 'display_height_px' => 768, 'display_number' => 1, ], [ 'type' => 'text_editor_20250728', 'name' => 'str_replace_based_edit_tool', ], [ 'type' => 'bash_20250124', 'name' => 'bash', ], ], betas: ['computer-use-2025-11-24'], ); echo $response; ``` ```ruby Ruby client = Anthropic::Client.new response = client.beta.messages.create( model: "claude-opus-5", max_tokens: 1024, tools: [ { type: "computer_20251124", name: "computer", display_width_px: 1024, display_height_px: 768, display_number: 1 }, { type: "text_editor_20250728", name: "str_replace_based_edit_tool" }, { type: "bash_20250124", name: "bash" } ], messages: [ { role: "user", content: "Save a picture of a cat to my desktop." } ], betas: ["computer-use-2025-11-24"] ) puts response ``` A beta header is only required for the computer use tool. The preceding example shows all three tools being used together, which requires the beta header because it includes the computer use tool. *** ## How computer use works * Add the computer use tool (and optionally other tools) to your API request. * Include a user prompt that requires desktop interaction, for example, "Save a picture of a cat to my desktop." * Claude assesses if the computer use tool can help with the user's query. * If yes, Claude constructs a properly formatted tool use request. * The API response has a `stop_reason` of `tool_use`, signaling a tool use request. * On your end, extract the tool name and input from Claude's request. * Use the tool on a container or virtual machine. * Continue the conversation with a new `user` message containing a `tool_result` content block. * Claude analyzes the tool results to determine if more tool use is needed or the task has been completed. * If Claude determines another tool is needed, it responds with another `tool_use` `stop_reason` and you should return to step 3. * Otherwise, it crafts a text response to the user. The repetition of steps 3 and 4 without user input is referred to as the "agent loop" (that is, Claude responding with a tool use request and your application responding to Claude with the results of evaluating that request). ### The computing environment Computer use requires a sandboxed computing environment where Claude can safely interact with applications and the web. This environment includes: 1. **Virtual display:** A virtual X11 display server (using Xvfb) that renders the desktop interface Claude will see through screenshots and control with mouse/keyboard actions. 2. **Desktop environment:** A lightweight UI with window manager (Mutter) and panel (Tint2) running on Linux, which provides a consistent graphical interface for Claude to interact with. 3. **Applications:** Pre-installed Linux applications such as Firefox, LibreOffice, text editors, and file managers that Claude can use to complete tasks. 4. **Tool implementations:** Integration code that translates Claude's abstract tool requests (such as "move mouse" or "take screenshot") into actual operations in the virtual environment. 5. **Agent loop:** A program that handles communication between Claude and the environment, sending Claude's actions to the environment and returning the results (screenshots, command outputs) back to Claude. When you use computer use, Claude doesn't directly connect to this environment. Instead, your application: 1. Receives Claude's tool use requests 2. Translates them into actions in your computing environment 3. Captures the results (such as screenshots and command outputs) 4. Returns these results to Claude For security and isolation, the reference implementation runs all of this inside a Docker container with appropriate port mappings for viewing and interacting with the environment. *** ## How to implement computer use ### Start with the reference implementation A [reference implementation](https://github.com/anthropics/anthropic-quickstarts/tree/main/computer-use-demo) is available that includes everything you need to get started with computer use: * A [containerized environment](https://github.com/anthropics/anthropic-quickstarts/blob/main/computer-use-demo/Dockerfile) suitable for computer use with Claude * Implementations of [the computer use tools](https://github.com/anthropics/anthropic-quickstarts/tree/main/computer-use-demo/computer_use_demo/tools) * An [agent loop](https://github.com/anthropics/anthropic-quickstarts/blob/main/computer-use-demo/computer_use_demo/loop.py) that interacts with the Claude API and runs the computer use tools * A web interface to interact with the container, agent loop, and tools. ### Understand the agent loop The core of computer use is the "agent loop": a cycle where Claude requests tool actions, your application runs them, and returns results to Claude. The loop uses the client you created in the [Quick start](https://platform.claude.com/docs/en/agents-and-tools/tool-use/computer-use-tool#quick-start), a tool list shaped like the Quick start's `tools` array, and the tool-call processing helper defined in [Process Claude's tool calls](https://platform.claude.com/docs/en/agents-and-tools/tool-use/computer-use-tool#implement-the-computer-use-tool). Here's a simplified example: ```bash cURL # The agent loop is a stateful, multi-turn pattern that doesn't translate to a # one-off shell command. See the SDK tabs for the implementation. ``` ```bash CLI # The agent loop is a stateful, multi-turn pattern that doesn't translate to a # one-off shell command. See the SDK tabs for the implementation. ``` ```python Python def sampling_loop(model, messages, max_iterations=10): """ Run the computer-use agent loop until Claude stops requesting tools or the iteration limit is reached. """ for _ in range(max_iterations): response = client.beta.messages.create( model=model, max_tokens=4096, messages=messages, tools=TOOLS, betas=["computer-use-2025-11-24"], ) # Add Claude's response to the conversation history messages.append({"role": "assistant", "content": response.content}) # Run any tools Claude requested and collect results tool_results = process_tool_calls(response) if not tool_results: return messages # No more tool use; task complete # Send tool results back to Claude for the next iteration messages.append({"role": "user", "content": tool_results}) return messages ``` ```typescript TypeScript async function samplingLoop( model: string, messages: Anthropic.Beta.BetaMessageParam[], maxIterations = 10, ): Promise { // Run the computer-use agent loop until Claude stops requesting tools // or the iteration limit is reached. for (let i = 0; i < maxIterations; i++) { const response = await client.beta.messages.create({ model, max_tokens: 4096, messages, tools, betas: ["computer-use-2025-11-24"], }); // Add Claude's response to the conversation history messages.push({ role: "assistant", content: response.content }); // Run any tools Claude requested and collect results const toolResults = processToolCalls(response); if (toolResults.length === 0) { return messages; // No more tool use; task complete } // Send tool results back to Claude for the next iteration messages.push({ role: "user", content: toolResults }); } return messages; } ``` ```csharp C# async Task> SamplingLoop( Model model, List messages, int maxIterations = 10 ) { // Run the computer-use agent loop until Claude stops requesting tools // or the iteration limit is reached. for (var i = 0; i < maxIterations; i++) { var response = await client.Beta.Messages.Create( new MessageCreateParams { Model = model, MaxTokens = 4096, Messages = messages, Tools = tools, Betas = ["computer-use-2025-11-24"], } ); // Add Claude's response to the conversation history messages.Add( new() { Role = Role.Assistant, Content = response .Content.Select(block => new BetaContentBlockParam(block.Json)) .ToList(), } ); // Run any tools Claude requested and collect results var toolResults = ProcessToolCalls(response); if (toolResults.Count == 0) { return messages; // No more tool use; task complete } // Send tool results back to Claude for the next iteration messages.Add(new() { Role = Role.User, Content = toolResults }); } return messages; } ``` ```go Go // samplingLoop runs the computer-use agent loop until Claude stops // requesting tools or the iteration limit is reached. func samplingLoop(ctx context.Context, model anthropic.Model, messages []anthropic.BetaMessageParam, maxIterations int) ([]anthropic.BetaMessageParam, error) { for range maxIterations { response, err := client.Beta.Messages.New(ctx, anthropic.BetaMessageNewParams{ Model: model, MaxTokens: 4096, Messages: messages, Tools: tools, Betas: []anthropic.AnthropicBeta{"computer-use-2025-11-24"}, }) if err != nil { return nil, err } // Add Claude's response to the conversation history messages = append(messages, response.ToParam()) // Run any tools Claude requested and collect results toolResults := processToolCalls(response) if len(toolResults) == 0 { return messages, nil // No more tool use; task complete } // Send tool results back to Claude for the next iteration messages = append(messages, anthropic.BetaMessageParam{ Role: anthropic.BetaMessageParamRoleUser, Content: toolResults, }) } return messages, nil } ``` ```java Java /** * Run the computer-use agent loop until Claude stops requesting tools * or the iteration limit is reached. */ List samplingLoop(Model model, List messages, int maxIterations) { for (int i = 0; i < maxIterations; i++) { BetaMessage response = client.beta().messages().create(MessageCreateParams.builder() .model(model) .maxTokens(4096) .messages(messages) .addTool(COMPUTER_TOOL) .addBeta("computer-use-2025-11-24") .build()); // Add Claude's response to the conversation history messages.add(BetaMessageParam.builder() .role(BetaMessageParam.Role.ASSISTANT) .contentOfBetaContentBlockParams( response.content().stream().map(BetaContentBlock::toParam).toList()) .build()); // Run any tools Claude requested and collect results List toolResults = processToolCalls(response); if (toolResults.isEmpty()) { return messages; // No more tool use; task complete } // Send tool results back to Claude for the next iteration messages.add(BetaMessageParam.builder() .role(BetaMessageParam.Role.USER) .contentOfBetaContentBlockParams(toolResults) .build()); } return messages; } ``` ```php PHP /** * Run the computer-use agent loop until Claude stops requesting tools * or the iteration limit is reached. */ function samplingLoop(string $model, array $messages, int $maxIterations = 10): array { global $client, $tools; for ($i = 0; $i < $maxIterations; $i++) { $response = $client->beta->messages->create( model: $model, maxTokens: 4096, messages: $messages, tools: $tools, betas: ['computer-use-2025-11-24'], ); // Add Claude's response to the conversation history $messages[] = BetaMessageParam::with(role: Role::ASSISTANT, content: $response->content); // Run any tools Claude requested and collect results $toolResults = processToolCalls($response); if ($toolResults === []) { return $messages; // No more tool use; task complete } // Send tool results back to Claude for the next iteration $messages[] = BetaMessageParam::with(role: Role::USER, content: $toolResults); } return $messages; } ``` ```ruby Ruby # Run the computer-use agent loop until Claude stops requesting tools # or the iteration limit is reached. def sampling_loop(model, messages, max_iterations: 10) max_iterations.times do response = CLIENT.beta.messages.create( model: model, max_tokens: 4096, messages: messages, tools: TOOLS, betas: ["computer-use-2025-11-24"] ) # Add Claude's response to the conversation history messages << {role: "assistant", content: response.content} # Run any tools Claude requested and collect results tool_results = process_tool_calls(response) return messages if tool_results.empty? # No more tool use; task complete # Send tool results back to Claude for the next iteration messages << {role: "user", content: tool_results} end messages end ``` The loop continues until either Claude responds without requesting any tools (task completion) or the maximum iteration limit is reached. This safeguard prevents potential infinite loops that could result in unexpected API costs. Try the reference implementation out before reading the rest of this documentation. ### Optimize model performance with prompting Here are some tips on how to get the best quality outputs: 1. Specify simple, well-defined tasks and provide explicit instructions for each step. 2. Claude sometimes assumes outcomes of its actions without explicitly checking their results. To prevent this you can prompt Claude with `After each step, take a screenshot and carefully evaluate if you have achieved the right outcome. Explicitly show your thinking: "I have evaluated step X..." If not correct, try again. Only when you confirm a step was executed correctly should you move on to the next one.` 3. Some UI elements (such as dropdowns and scrollbars) might be tricky for Claude to manipulate using mouse movements. If you experience this, try prompting the model to use keyboard shortcuts. 4. For repeatable tasks or UI interactions, include example screenshots and tool calls of successful outcomes in your prompt. 5. If you need the model to log in, provide it with the username and password in your prompt inside XML tags such as ``. Using computer use within applications that require login increases the risk of bad outcomes as a result of prompt injection. Review [Mitigate jailbreaks and prompt injections](https://platform.claude.com/docs/en/test-and-evaluate/strengthen-guardrails/mitigate-jailbreaks) before providing the model with login credentials. 6. When constructing a user turn's `content` array, place the instruction text *before* the screenshot image. Providing the target description before the image is processed improves click accuracy. 7. When using `computer_20251124` with `enable_zoom: true` set, Claude zooms in on a region when asked about small text or specific UI elements that aren't legible at the screenshot's default resolution, such as file names in a sidebar, tab titles, status-bar text, line numbers, or button labels. If Claude isn't zooming when you expect, ask about a specific region or element rather than the screen as a whole. If you repeatedly encounter a clear set of issues or know in advance the tasks Claude will need to complete, use the system prompt to provide Claude with explicit tips or instructions on how to do the tasks successfully. For agents that span multiple sessions, run end-to-end verification at the start of each session, not only after implementation. Browser-based checks catch regressions from prior sessions that code-level review alone misses. See [Effective harnesses for long-running agents](https://www.anthropic.com/engineering/effective-harnesses-for-long-running-agents) for details. ### System prompts When one of the Anthropic-schema tools is requested through the Claude API, a computer use-specific system prompt is generated. It's similar to the [tool use system prompt](https://platform.claude.com/docs/en/agents-and-tools/tool-use/define-tools#tool-use-system-prompt) but starts with: > You have access to a set of functions you can use to answer the user's question. This includes access to a sandboxed computing environment. You do NOT currently have the ability to inspect files or interact with external resources, except by invoking the below functions. As with regular tool use, the user-provided `system` parameter is still respected and used in the construction of the combined system prompt. ### Available actions The computer use tool supports these actions: **Basic actions (all versions)** * **screenshot:** Capture the current display * **left\_click:** Click at coordinates `[x, y]` * **type:** Type text string * **key:** Press key or key combination (for example, "ctrl+s") * **mouse\_move:** Move cursor to coordinates **Enhanced actions (`computer_20250124` and later)** Available in `computer_20250124` and `computer_20251124`: * **scroll:** Scroll in any direction with amount control * **left\_click\_drag:** Click and drag between coordinates * **right\_click**, **middle\_click:** Additional mouse buttons * **double\_click**, **triple\_click:** Multiple clicks * **left\_mouse\_down**, **left\_mouse\_up:** Fine-grained click control * **hold\_key:** Hold down a key for a specified duration (in seconds) * **wait:** Pause between actions **Enhanced actions (`computer_20251124`)** Available in Claude Opus 5, Claude Sonnet 5, Claude Opus 4.8, Claude Opus 4.7, Claude Opus 4.6, Claude Sonnet 4.6, and Claude Opus 4.5: * All actions from `computer_20250124` * **zoom:** View a specific region of the screen at full resolution. Requires `enable_zoom: true` in tool definition. Takes a `region` parameter with coordinates `[x1, y1, x2, y2]` defining top-left and bottom-right corners of the area to inspect. Take a screenshot: ```json { "action": "screenshot" } ``` Click at position: ```json { "action": "left_click", "coordinate": [500, 300] } ``` Type text: ```json { "action": "type", "text": "Hello, world!" } ``` Scroll down: ```json { "action": "scroll", "coordinate": [500, 400], "scroll_direction": "down", "scroll_amount": 3 } ``` Zoom to view region in detail (Claude Opus 5, Sonnet 5, Opus 4.8, Opus 4.7, Opus 4.6, Sonnet 4.6, and Opus 4.5): ```json { "action": "zoom", "region": [100, 200, 400, 350] } ``` To hold modifier keys (such as Shift, Ctrl, or Alt) while performing click or scroll actions, use the `text` parameter on those actions. This is different from `hold_key`, which holds a key for a duration without performing other actions. Shift+click (for example, to select a range of items): ```json { "action": "left_click", "coordinate": [500, 300], "text": "shift" } ``` Ctrl+click (for example, to multi-select on Windows/Linux): ```json { "action": "left_click", "coordinate": [500, 300], "text": "ctrl" } ``` Cmd+click (for example, to multi-select on macOS): ```json { "action": "left_click", "coordinate": [500, 300], "text": "super" } ``` Shift+scroll (for example, to scroll horizontally): ```json { "action": "scroll", "coordinate": [500, 400], "scroll_direction": "down", "scroll_amount": 3, "text": "shift" } ``` The `text` parameter in click/scroll actions accepts modifier keys such as `shift`, `ctrl`, `alt`, and `super` (for the Command/Windows key). ### Tool parameters | Parameter | Required | Description | | ------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `type` | Yes | Tool version (`computer_20251124` or `computer_20250124`) | | `name` | Yes | Must be "computer" | | `display_width_px` | Yes | Display width in pixels | | `display_height_px` | Yes | Display height in pixels | | `display_number` | No | Display number for X11 environments | | `enable_zoom` | No | Enable zoom action (`computer_20251124` only). Set to `true` to allow Claude to zoom into specific screen regions. Default: `false` | **Important:** Your application must explicitly run the computer use tool; Claude cannot run it directly. You are responsible for implementing the screenshot capture, mouse movements, keyboard inputs, and other actions based on Claude's requests. ### Combining with thinking For combining computer use with thinking, see [Thinking](https://platform.claude.com/docs/en/build-with-claude/thinking). For computer use specifically, internal benchmarking suggests these `effort` settings: * **Claude Opus 4.7:** use `high` as the default; use `low` for high-throughput or cost-sensitive workloads. * **Claude Sonnet 4.6 and Claude Opus 4.6:** use `medium` as the default (best accuracy-to-cost ratio). Avoid `max`, which adds token cost without improving accuracy on UI tasks. On these models, `low` uses *fewer* output tokens than disabling thinking entirely (fewer mistakes mean fewer retries), making it a strong option for cost-sensitive loops. ### Augmenting computer use with other tools To add other tools alongside computer use, include them in the same `tools` array. The [Quick start](https://platform.claude.com/docs/en/agents-and-tools/tool-use/computer-use-tool#quick-start) section shows this pattern with the [bash tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/bash-tool) and [text editor tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/text-editor-tool). You can add your own [custom tool definitions](https://platform.claude.com/docs/en/agents-and-tools/tool-use/define-tools) the same way. ### Build a custom computer use environment The [reference implementation](https://github.com/anthropics/anthropic-quickstarts/tree/main/computer-use-demo) is meant to help you get started with computer use. It includes all of the components needed to have Claude use a computer. However, you can build your own environment for computer use to suit your needs. You'll need: * A virtualized or containerized environment suitable for computer use with Claude * An implementation of at least one of the Anthropic-schema computer use tools * An agent loop that interacts with the Claude API and runs the `tool_use` results using your tool implementations * An API or UI that allows user input to start the agent loop #### Implement the computer use tool The computer use tool is implemented as a schema-less tool. When using this tool, you don't need to provide an input schema as with other tools; the schema is built into Claude's model and can't be modified. Create a virtual display or connect to an existing display that Claude will interact with. This typically involves setting up Xvfb (X Virtual Framebuffer) or similar technology. Create functions to handle each action type that Claude might request: ```bash cURL # This is application-side helper code with no API request. See the SDK tabs # for the pattern. ``` ```bash CLI # This is application-side helper code with no API request. See the SDK tabs # for the pattern. ``` ```python Python def capture_screenshot(): return "" def click_at(x, y): return f"clicked at ({x}, {y})" def type_text(text): return f"typed: {text}" def handle_computer_action(action_type, params): if action_type == "screenshot": return capture_screenshot() elif action_type == "left_click": x, y = params["coordinate"] return click_at(x, y) elif action_type == "type": return type_text(params["text"]) # Handle other actions as needed return f"unhandled action: {action_type}" ``` ```typescript TypeScript function captureScreenshot(): string { return ""; } function clickAt(x: number, y: number): string { return `clicked at (${x}, ${y})`; } function typeText(text: string): string { return `typed: ${text}`; } function handleComputerAction( actionType: string, params: Record, ): string { if (actionType === "screenshot") { return captureScreenshot(); } else if (actionType === "left_click") { const [x, y] = params.coordinate as [number, number]; return clickAt(x, y); } else if (actionType === "type") { return typeText(params.text as string); } // Handle other actions as needed return `unhandled action: ${actionType}`; } ``` ```csharp C# string CaptureScreenshot() => ""; string ClickAt(int x, int y) => $"clicked at ({x}, {y})"; string TypeText(string text) => $"typed: {text}"; string HandleComputerAction(string actionType, IReadOnlyDictionary input) => actionType switch { "screenshot" => CaptureScreenshot(), "left_click" => ClickAt( input["coordinate"][0].GetInt32(), input["coordinate"][1].GetInt32() ), "type" => TypeText(input["text"].GetString()!), // Handle other actions as needed _ => $"unhandled action: {actionType}", }; ``` ```go Go func captureScreenshot() string { return "" } func clickAt(x, y int) string { return fmt.Sprintf("clicked at (%d, %d)", x, y) } func typeText(text string) string { return fmt.Sprintf("typed: %s", text) } func handleComputerAction(actionType string, params map[string]any) string { switch actionType { case "screenshot": return captureScreenshot() case "left_click": coord := params["coordinate"].([]any) return clickAt(int(coord[0].(float64)), int(coord[1].(float64))) case "type": return typeText(params["text"].(string)) // Handle other actions as needed default: return fmt.Sprintf("unhandled action: %s", actionType) } } ``` ```java Java String captureScreenshot() { return ""; } String clickAt(long x, long y) { return "clicked at (" + x + ", " + y + ")"; } String typeText(String text) { return "typed: " + text; } String handleComputerAction(String actionType, Map params) { return switch (actionType) { case "screenshot" -> captureScreenshot(); case "left_click" -> { List coordinate = (List) params.get("coordinate").asArray().get(); long x = ((Number) coordinate.get(0).asNumber().get()).longValue(); long y = ((Number) coordinate.get(1).asNumber().get()).longValue(); yield clickAt(x, y); } case "type" -> typeText(params.get("text").asStringOrThrow()); // Handle other actions as needed default -> "unhandled action: " + actionType; }; } ``` ```php PHP function captureScreenshot(): string { return ''; } function clickAt(int $x, int $y): string { return "clicked at ({$x}, {$y})"; } function typeText(string $text): string { return "typed: {$text}"; } function handleComputerAction(string $actionType, array $params): string { return match ($actionType) { 'screenshot' => captureScreenshot(), 'left_click' => clickAt(...$params['coordinate']), 'type' => typeText($params['text']), // Handle other actions as needed default => "unhandled action: {$actionType}", }; } ``` ```ruby Ruby def capture_screenshot "" end def click_at(x, y) "clicked at (#{x}, #{y})" end def type_text(text) "typed: #{text}" end def handle_computer_action(action_type, params) case action_type when "screenshot" capture_screenshot when "left_click" x, y = params[:coordinate] click_at(x, y) when "type" type_text(params[:text]) # Handle other actions as needed else "unhandled action: #{action_type}" end end ``` Extract and run tool calls from Claude's responses: ```bash cURL # This is application-side helper code with no API request. See the SDK tabs # for the pattern. ``` ```bash CLI # This is application-side helper code with no API request. See the SDK tabs # for the pattern. ``` ```python Python def process_tool_calls(response): tool_results = [] for block in response.content: if block.type == "tool_use": action = block.input["action"] result = handle_computer_action(action, block.input) tool_results.append( { "type": "tool_result", "tool_use_id": block.id, "content": result, } ) return tool_results ``` ```typescript TypeScript function processToolCalls( response: Anthropic.Beta.BetaMessage, ): Anthropic.Beta.BetaToolResultBlockParam[] { const toolResults: Anthropic.Beta.BetaToolResultBlockParam[] = []; for (const block of response.content) { if (block.type === "tool_use") { const input = block.input as Record; const action = input.action as string; const result = handleComputerAction(action, input); toolResults.push({ type: "tool_result", tool_use_id: block.id, content: result, }); } } return toolResults; } ``` ```csharp C# List ProcessToolCalls(BetaMessage response) { List toolResults = []; foreach (var block in response.Content) { if (block.TryPickToolUse(out var toolUse)) { var action = toolUse.Input["action"].GetString()!; var result = HandleComputerAction(action, toolUse.Input); toolResults.Add(new BetaToolResultBlockParam(toolUse.ID) { Content = result }); } } return toolResults; } ``` ```go Go func processToolCalls(response *anthropic.BetaMessage) []anthropic.BetaContentBlockParamUnion { var toolResults []anthropic.BetaContentBlockParamUnion for _, block := range response.Content { switch variant := block.AsAny().(type) { case anthropic.BetaToolUseBlock: input := variant.Input.(map[string]any) action := input["action"].(string) result := handleComputerAction(action, input) toolResults = append(toolResults, anthropic.NewBetaToolResultBlock(variant.ID, result, false)) } } return toolResults } ``` ```java Java List processToolCalls(BetaMessage response) { List toolResults = new ArrayList<>(); for (BetaContentBlock block : response.content()) { if (block.isToolUse()) { BetaToolUseBlock toolUse = block.asToolUse(); Map input = (Map) toolUse._input().asObject().get(); String action = input.get("action").asStringOrThrow(); String result = handleComputerAction(action, input); toolResults.add(BetaContentBlockParam.ofToolResult( BetaToolResultBlockParam.builder() .toolUseId(toolUse.id()) .content(result) .build())); } } return toolResults; } ``` ```php PHP function processToolCalls(BetaMessage $response): array { $toolResults = []; foreach ($response->content as $block) { if ($block instanceof BetaToolUseBlock) { $action = $block->input['action']; $result = handleComputerAction($action, $block->input); $toolResults[] = BetaToolResultBlockParam::with( toolUseID: $block->id, content: $result, ); } } return $toolResults; } ``` ```ruby Ruby def process_tool_calls(response) tool_results = [] response.content.each do |block| next unless block.type == :tool_use action = block.input[:action] result = handle_computer_action(action, block.input) tool_results << { type: "tool_result", tool_use_id: block.id, content: result } end tool_results end ``` Create a loop that continues until Claude completes the task: ```bash cURL # The agent loop is a stateful, multi-turn pattern that doesn't translate to a # one-off shell command. See the SDK tabs for the implementation. ``` ```bash CLI # The agent loop is a stateful, multi-turn pattern that doesn't translate to a # one-off shell command. See the SDK tabs for the implementation. ``` ```python Python def sampling_loop(model, messages, max_iterations=10): """ Run the computer-use agent loop until Claude stops requesting tools or the iteration limit is reached. """ for _ in range(max_iterations): response = client.beta.messages.create( model=model, max_tokens=4096, messages=messages, tools=TOOLS, betas=["computer-use-2025-11-24"], ) # Add Claude's response to the conversation history messages.append({"role": "assistant", "content": response.content}) # Run any tools Claude requested and collect results tool_results = process_tool_calls(response) if not tool_results: return messages # No more tool use; task complete # Send tool results back to Claude for the next iteration messages.append({"role": "user", "content": tool_results}) return messages ``` ```typescript TypeScript async function samplingLoop( model: string, messages: Anthropic.Beta.BetaMessageParam[], maxIterations = 10, ): Promise { // Run the computer-use agent loop until Claude stops requesting tools // or the iteration limit is reached. for (let i = 0; i < maxIterations; i++) { const response = await client.beta.messages.create({ model, max_tokens: 4096, messages, tools, betas: ["computer-use-2025-11-24"], }); // Add Claude's response to the conversation history messages.push({ role: "assistant", content: response.content }); // Run any tools Claude requested and collect results const toolResults = processToolCalls(response); if (toolResults.length === 0) { return messages; // No more tool use; task complete } // Send tool results back to Claude for the next iteration messages.push({ role: "user", content: toolResults }); } return messages; } ``` ```csharp C# async Task> SamplingLoop( Model model, List messages, int maxIterations = 10 ) { // Run the computer-use agent loop until Claude stops requesting tools // or the iteration limit is reached. for (var i = 0; i < maxIterations; i++) { var response = await client.Beta.Messages.Create( new MessageCreateParams { Model = model, MaxTokens = 4096, Messages = messages, Tools = tools, Betas = ["computer-use-2025-11-24"], } ); // Add Claude's response to the conversation history messages.Add( new() { Role = Role.Assistant, Content = response .Content.Select(block => new BetaContentBlockParam(block.Json)) .ToList(), } ); // Run any tools Claude requested and collect results var toolResults = ProcessToolCalls(response); if (toolResults.Count == 0) { return messages; // No more tool use; task complete } // Send tool results back to Claude for the next iteration messages.Add(new() { Role = Role.User, Content = toolResults }); } return messages; } ``` ```go Go // samplingLoop runs the computer-use agent loop until Claude stops // requesting tools or the iteration limit is reached. func samplingLoop(ctx context.Context, model anthropic.Model, messages []anthropic.BetaMessageParam, maxIterations int) ([]anthropic.BetaMessageParam, error) { for range maxIterations { response, err := client.Beta.Messages.New(ctx, anthropic.BetaMessageNewParams{ Model: model, MaxTokens: 4096, Messages: messages, Tools: tools, Betas: []anthropic.AnthropicBeta{"computer-use-2025-11-24"}, }) if err != nil { return nil, err } // Add Claude's response to the conversation history messages = append(messages, response.ToParam()) // Run any tools Claude requested and collect results toolResults := processToolCalls(response) if len(toolResults) == 0 { return messages, nil // No more tool use; task complete } // Send tool results back to Claude for the next iteration messages = append(messages, anthropic.BetaMessageParam{ Role: anthropic.BetaMessageParamRoleUser, Content: toolResults, }) } return messages, nil } ``` ```java Java /** * Run the computer-use agent loop until Claude stops requesting tools * or the iteration limit is reached. */ List samplingLoop(Model model, List messages, int maxIterations) { for (int i = 0; i < maxIterations; i++) { BetaMessage response = client.beta().messages().create(MessageCreateParams.builder() .model(model) .maxTokens(4096) .messages(messages) .addTool(COMPUTER_TOOL) .addBeta("computer-use-2025-11-24") .build()); // Add Claude's response to the conversation history messages.add(BetaMessageParam.builder() .role(BetaMessageParam.Role.ASSISTANT) .contentOfBetaContentBlockParams( response.content().stream().map(BetaContentBlock::toParam).toList()) .build()); // Run any tools Claude requested and collect results List toolResults = processToolCalls(response); if (toolResults.isEmpty()) { return messages; // No more tool use; task complete } // Send tool results back to Claude for the next iteration messages.add(BetaMessageParam.builder() .role(BetaMessageParam.Role.USER) .contentOfBetaContentBlockParams(toolResults) .build()); } return messages; } ``` ```php PHP /** * Run the computer-use agent loop until Claude stops requesting tools * or the iteration limit is reached. */ function samplingLoop(string $model, array $messages, int $maxIterations = 10): array { global $client, $tools; for ($i = 0; $i < $maxIterations; $i++) { $response = $client->beta->messages->create( model: $model, maxTokens: 4096, messages: $messages, tools: $tools, betas: ['computer-use-2025-11-24'], ); // Add Claude's response to the conversation history $messages[] = BetaMessageParam::with(role: Role::ASSISTANT, content: $response->content); // Run any tools Claude requested and collect results $toolResults = processToolCalls($response); if ($toolResults === []) { return $messages; // No more tool use; task complete } // Send tool results back to Claude for the next iteration $messages[] = BetaMessageParam::with(role: Role::USER, content: $toolResults); } return $messages; } ``` ```ruby Ruby # Run the computer-use agent loop until Claude stops requesting tools # or the iteration limit is reached. def sampling_loop(model, messages, max_iterations: 10) max_iterations.times do response = CLIENT.beta.messages.create( model: model, max_tokens: 4096, messages: messages, tools: TOOLS, betas: ["computer-use-2025-11-24"] ) # Add Claude's response to the conversation history messages << {role: "assistant", content: response.content} # Run any tools Claude requested and collect results tool_results = process_tool_calls(response) return messages if tool_results.empty? # No more tool use; task complete # Send tool results back to Claude for the next iteration messages << {role: "user", content: tool_results} end messages end ``` #### Handle errors When implementing the computer use tool, various errors might occur. Here's how to handle them: If screenshot capture fails, return an appropriate error message: ```json { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_01A09q90qw90lq917835lq9", "content": "Error: Failed to capture screenshot. Display may be locked or unavailable.", "is_error": true } ] } ``` If Claude provides coordinates outside the display bounds: ```json { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_01A09q90qw90lq917835lq9", "content": "Error: Coordinates (1200, 900) are outside display bounds (1024x768).", "is_error": true } ] } ``` If an action fails to run: ```json { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_01A09q90qw90lq917835lq9", "content": "Error: Failed to perform click action. The application may be unresponsive.", "is_error": true } ] } ``` #### Size screenshots to fit image limits Screenshots sent to the computer tool should fit within Claude's image size limits (see [image size limits](https://platform.claude.com/docs/en/build-with-claude/vision#evaluate-image-size)). The API downscales oversized images before Claude sees them, and Claude returns coordinates for the image it sees, so relying on the server-side downscale leaves you without the scale factor you need to map those coordinates back to your screen. Only images over the API's separate [request limits](https://platform.claude.com/docs/en/build-with-claude/vision#request-limits) (for example, more than 8,000 px on a side) are rejected with a validation error rather than downscaled. Limits vary by model. Claude Opus 5, Claude Sonnet 5, Claude Opus 4.8, and Claude Opus 4.7 accept up to 2576 pixels on the long edge; earlier models accept up to 1568 pixels on the long edge and approximately 1.15 megapixels total. The following example uses the earlier-model 1568 px / 1.15 MP limits; substitute your model's limit. If your screen is larger than the limit, resize the screenshot before sending it, set `display_width_px`/`display_height_px` to the resized dimensions, and scale Claude's returned coordinates back to the original screen space: ```bash cURL # Coordinate scaling and screenshot resizing happen in your application code, not # in the API request. See the SDK tabs for the helper pattern. ``` ```bash CLI # Coordinate scaling and screenshot resizing happen in your application code, not # in the API request. See the SDK tabs for the helper pattern. ``` ```python Python import math def get_scale_factor(width, height): """Calculate scale factor to meet API constraints.""" long_edge = max(width, height) total_pixels = width * height long_edge_scale = 1568 / long_edge total_pixels_scale = math.sqrt(1_150_000 / total_pixels) return min(1.0, long_edge_scale, total_pixels_scale) # When capturing screenshot scale = get_scale_factor(screen_width, screen_height) scaled_width = int(screen_width * scale) scaled_height = int(screen_height * scale) # Resize image to scaled dimensions before sending to Claude screenshot = capture_and_resize(scaled_width, scaled_height) # When handling Claude's coordinates, scale them back up def execute_click(x, y): screen_x = x / scale screen_y = y / scale perform_click(screen_x, screen_y) ``` ```typescript TypeScript const MAX_LONG_EDGE = 1568; const MAX_PIXELS = 1_150_000; function getScaleFactor(width: number, height: number): number { const longEdge = Math.max(width, height); const totalPixels = width * height; const longEdgeScale = MAX_LONG_EDGE / longEdge; const totalPixelsScale = Math.sqrt(MAX_PIXELS / totalPixels); return Math.min(1.0, longEdgeScale, totalPixelsScale); } // When capturing screenshot const scale = getScaleFactor(screenWidth, screenHeight); const scaledWidth = Math.floor(screenWidth * scale); const scaledHeight = Math.floor(screenHeight * scale); // Resize image to scaled dimensions before sending to Claude const screenshot = captureAndResize(scaledWidth, scaledHeight); // When handling Claude's coordinates, scale them back up function executeClick(x: number, y: number): void { const screenX = x / scale; const screenY = y / scale; performClick(screenX, screenY); } ``` ```csharp C# double GetScaleFactor(int width, int height) { // Calculate scale factor to meet API constraints. int longEdge = Math.Max(width, height); int totalPixels = width * height; double longEdgeScale = 1568.0 / longEdge; double totalPixelsScale = Math.Sqrt(1_150_000.0 / totalPixels); return Math.Min(1.0, Math.Min(longEdgeScale, totalPixelsScale)); } // When capturing screenshot double scale = GetScaleFactor(screenWidth, screenHeight); int scaledWidth = (int)(screenWidth * scale); int scaledHeight = (int)(screenHeight * scale); // Resize image to scaled dimensions before sending to Claude var screenshot = CaptureAndResize(scaledWidth, scaledHeight); // When handling Claude's coordinates, scale them back up void ExecuteClick(int x, int y) { double screenX = x / scale; double screenY = y / scale; PerformClick(screenX, screenY); } ``` ```go Go func getScaleFactor(width, height int) float64 { longest := float64(max(width, height)) area := float64(width * height) return min(1.0, 1568/longest, math.Sqrt(1_150_000/area)) } // ... // When capturing screenshot scale := getScaleFactor(screenWidth, screenHeight) scaledWidth := int(float64(screenWidth) * scale) scaledHeight := int(float64(screenHeight) * scale) // Resize image to scaled dimensions before sending to Claude screenshot := captureAndResize(scaledWidth, scaledHeight) // When handling Claude's coordinates, scale them back up executeClick := func(x, y int) { performClick(float64(x)/scale, float64(y)/scale) } ``` ```java Java static double getScaleFactor(int width, int height) { return Math.min( 1.0, Math.min( 1568.0 / Math.max(width, height), Math.sqrt(1_150_000.0 / (width * height)) ) ); } void main() { // ... // When capturing screenshot double scale = getScaleFactor(screenWidth, screenHeight); int scaledWidth = (int)(screenWidth * scale); int scaledHeight = (int)(screenHeight * scale); // Resize image to scaled dimensions before sending to Claude var screenshot = captureAndResize(scaledWidth, scaledHeight); // When handling Claude's coordinates, scale them back up BiConsumer executeClick = (x, y) -> performClick(x / scale, y / scale); // ... } ``` ```php PHP function getScaleFactor(int $width, int $height): float { return min( 1.0, 1568 / max($width, $height), sqrt(1_150_000 / ($width * $height)), ); } // ... // When capturing screenshot $scale = getScaleFactor($screenWidth, $screenHeight); $scaledWidth = (int)($screenWidth * $scale); $scaledHeight = (int)($screenHeight * $scale); // Resize image to scaled dimensions before sending to Claude $screenshot = captureAndResize($scaledWidth, $scaledHeight); // When handling Claude's coordinates, scale them back up $executeClick = fn(int $x, int $y) => performClick($x / $scale, $y / $scale); ``` ```ruby Ruby def get_scale_factor(width, height) [1.0, 1568.0 / [width, height].max, Math.sqrt(1_150_000.0 / (width * height))].min end # ... # When capturing screenshot scale = get_scale_factor(screen_width, screen_height) scaled_width = (screen_width * scale).to_i scaled_height = (screen_height * scale).to_i # Resize image to scaled dimensions before sending to Claude screenshot = capture_and_resize(scaled_width, scaled_height) # When handling Claude's coordinates, scale them back up execute_click = ->(x, y) { perform_click(x / scale, y / scale) } ``` **macOS Retina displays** capture screenshots at a device pixel ratio of 2, so the image is twice the resolution of the logical screen coordinates. Either downscale the screenshot by 2x before sending, or halve the coordinates Claude returns before issuing the click. #### Diagnose click issues If clicks miss their targets, the cause is usually one of the following: | Symptom | Likely cause | Try | | ------------------------------------------------- | --------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | Clicks consistently offset in one direction | `display_width_px`/`display_height_px` don't match the image dimensions actually sent | Ensure display dimensions exactly match the screenshot you send | | Clicks land in the right area but miss the target | Target is very small, detail was lost downscaling a 4K+ source, or aspect ratio was distorted | Set `enable_zoom: true`; capture at lower DPI or crop to the relevant region; preserve aspect ratio when resizing | | Claude clicks the wrong element entirely | Ambiguous instruction, or visually similar elements nearby | Use positional prompts ("the blue Submit button in the bottom-right"); break the interaction into smaller steps | | Accuracy is consistently poor | Resolution too low | Try 1280x720 as a baseline | **Model choice affects click precision.** Claude Sonnet 4.6 is more mechanically precise at clicking than Claude Opus 4.6 and is more robust when screenshots require heavy downscaling. Claude Opus 4.7 narrows that gap: its click precision is roughly comparable to Sonnet 4.6, and its higher resolution limit means less downscaling is needed. #### Follow implementation best practices Set display dimensions that match your use case while staying within recommended limits: * For general desktop tasks: 1024x768 or 1280x720 * For web applications: 1280x800 or 1366x768 * Avoid resolutions above 1920x1080 to prevent performance issues When returning screenshots to Claude: * Encode screenshots as base64 PNG or JPEG * Consider compressing large screenshots to improve performance * Include relevant metadata such as timestamp or display state * If using higher resolutions, ensure coordinates are accurately scaled A screenshot goes back as an image content block inside the `tool_result` content array (see [Handle tool calls](https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls)): ```json { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_01A09q90qw90lq917835lq9", "content": [ { "type": "image", "source": { "type": "base64", "media_type": "image/png", "data": "iVBORw0KGgo..." } } ] } ] } ``` Long agent loops accumulate screenshots quickly (roughly 1,000–1,800 input tokens each). To keep [Prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) effective while bounding context: * Place one `cache_control` breakpoint after the system prompt and tool definitions, and up to three more on the most recent `tool_result` blocks, advancing them each turn. * Prune old screenshots in *batches*, not one each turn. Dropping a screenshot every turn changes the prefix every turn and invalidates the cache. A reasonable default is to keep the last three screenshots and prune every 25 turns, so the prefix stays byte-identical between prune events. Some applications need time to respond to actions: ```bash cURL # This is application-side helper code with no API request. See the SDK tabs for # the pattern. ``` ```bash CLI # This is application-side helper code with no API request. See the SDK tabs for # the pattern. ``` ```python Python def click_and_wait(x, y, wait_time=0.5): click_at(x, y) time.sleep(wait_time) # Allow UI to update ``` ```typescript TypeScript async function clickAndWait(x: number, y: number, waitMs = 500): Promise { clickAt(x, y); await setTimeout(waitMs); // Allow UI to update } ``` ```csharp C# static void ClickAndWait(int x, int y, double waitSeconds = 0.5) { ClickAt(x, y); Thread.Sleep(TimeSpan.FromSeconds(waitSeconds)); // Allow UI to update } ``` ```go Go func clickAndWaitFor(x, y int, wait time.Duration) { clickAt(x, y) time.Sleep(wait) // Allow UI to update } func clickAndWait(x, y int) { clickAndWaitFor(x, y, 500*time.Millisecond) } ``` ```java Java void clickAndWait(int x, int y) throws InterruptedException { clickAndWait(x, y, 500); } void clickAndWait(int x, int y, long waitTimeMillis) throws InterruptedException { clickAt(x, y); Thread.sleep(waitTimeMillis); // Allow UI to update } ``` ```php PHP function clickAndWait(int $x, int $y, float $waitSeconds = 0.5): void { clickAt($x, $y); usleep((int) ($waitSeconds * 1_000_000)); // Allow UI to update } ``` ```ruby Ruby def click_and_wait(x, y, wait_time: 0.5) click_at(x, y) sleep(wait_time) # Allow UI to update end ``` Check that requested actions are safe and valid: ```bash cURL # This is application-side helper code with no API request. See the SDK tabs for # the pattern. ``` ```bash CLI # This is application-side helper code with no API request. See the SDK tabs for # the pattern. ``` ```python Python def validate_action(action_type, params): if action_type == "left_click": x, y = params.get("coordinate", (0, 0)) if not (0 <= x < display_width and 0 <= y < display_height): return False, "Coordinates out of bounds" return True, None ``` ```typescript TypeScript interface ActionParams { coordinate?: [number, number]; } function validateAction(actionType: string, params: ActionParams): [boolean, string | null] { if (actionType === "left_click") { const [x, y] = params.coordinate ?? [0, 0]; if (!(x >= 0 && x < displayWidth && y >= 0 && y < displayHeight)) { return [false, "Coordinates out of bounds"]; } } return [true, null]; } ``` ```csharp C# const int DisplayWidth = 1024; const int DisplayHeight = 768; // ... static (bool IsValid, string? Error) ValidateAction(string actionType, IReadOnlyDictionary parameters) { if (actionType == "left_click") { int x = parameters["coordinate"][0].GetInt32(); int y = parameters["coordinate"][1].GetInt32(); if (x is < 0 or >= DisplayWidth || y is < 0 or >= DisplayHeight) { return (false, "Coordinates out of bounds"); } } return (true, null); } ``` ```go Go const ( displayWidth = 1024 displayHeight = 768 ) func validateAction(actionType string, params map[string]any) (bool, string) { if actionType == "left_click" { coord, ok := params["coordinate"].([]any) if !ok || len(coord) != 2 { return false, "Invalid coordinate" } x, y := int(coord[0].(float64)), int(coord[1].(float64)) if !(0 <= x && x < displayWidth && 0 <= y && y < displayHeight) { return false, "Coordinates out of bounds" } } return true, "" } ``` ```java Java static final int DISPLAY_WIDTH = 1024; static final int DISPLAY_HEIGHT = 768; record Validation(boolean valid, String error) {} Validation validateAction(String actionType, Map params) { if (actionType.equals("left_click")) { List coord = (List) params.get("coordinate").asArray().get(); long x = ((Number) coord.get(0).asNumber().get()).longValue(); long y = ((Number) coord.get(1).asNumber().get()).longValue(); if (!(0 <= x && x < DISPLAY_WIDTH && 0 <= y && y < DISPLAY_HEIGHT)) { return new Validation(false, "Coordinates out of bounds"); } } return new Validation(true, null); } ``` ```php PHP const DISPLAY_WIDTH = 1024; const DISPLAY_HEIGHT = 768; /** @return array{bool, ?string} */ function validateAction(string $actionType, array $params): array { if ($actionType === 'left_click') { [$x, $y] = $params['coordinate'] ?? [0, 0]; if (!(0 <= $x && $x < DISPLAY_WIDTH && 0 <= $y && $y < DISPLAY_HEIGHT)) { return [false, 'Coordinates out of bounds']; } } return [true, null]; } ``` ```ruby Ruby DISPLAY_WIDTH = 1024 DISPLAY_HEIGHT = 768 def validate_action(action_type, params) if action_type == "left_click" x, y = params.fetch(:coordinate, [0, 0]) unless (0...DISPLAY_WIDTH).cover?(x) && (0...DISPLAY_HEIGHT).cover?(y) return [false, "Coordinates out of bounds"] end end [true, nil] end ``` Keep a log of all actions for troubleshooting: ```bash cURL # This is application-side helper code with no API request. See the SDK tabs for # the pattern. ``` ```bash CLI # This is application-side helper code with no API request. See the SDK tabs for # the pattern. ``` ```python Python import logging def log_action(action_type, params, result): logging.info(f"Action: {action_type}, Params: {params}, Result: {result}") ``` ```typescript TypeScript function logAction(actionType: string, params: unknown, result: unknown): void { console.error( `Action: ${actionType}, Params: ${JSON.stringify(params)}, Result: ${JSON.stringify( result )}` ); } ``` ```csharp C# static void LogAction(string actionType, object? parameters, object? result) { Console.Error.WriteLine($"Action: {actionType}, Params: {parameters}, Result: {result}"); } ``` ```go Go func logAction(actionType string, params map[string]any, result any) { log.Printf("Action: %s, Params: %v, Result: %v", actionType, params, result) } ``` ```java Java import static java.lang.System.Logger.Level.INFO; static final System.Logger LOGGER = System.getLogger("computer-use"); void logAction(String actionType, Object params, Object result) { LOGGER.log(INFO, "Action: {0}, Params: {1}, Result: {2}", actionType, params, result); } ``` ```php PHP function logAction(string $actionType, array $params, mixed $result): void { error_log(sprintf( 'Action: %s, Params: %s, Result: %s', $actionType, json_encode($params), json_encode($result), )); } ``` ```ruby Ruby require "logger" LOGGER = Logger.new($stderr) def log_action(action_type, params, result) LOGGER.info("Action: #{action_type}, Params: #{params}, Result: #{result}") end ``` *** ## Understand computer use limitations Computer use is in beta. Keep the following limitations in mind: 1. **Latency:** The current computer use latency for human-AI interactions might be too slow compared to regular human-directed computer actions. Focus on use cases where speed isn't critical (for example, background information gathering, automated software testing) in trusted environments. 2. **Computer vision accuracy and reliability:** Claude might make mistakes or hallucinate when outputting specific coordinates while generating actions. Extended thinking can help you understand the model's reasoning and identify potential issues. 3. **Tool selection accuracy and reliability:** Claude might make mistakes or hallucinate when selecting tools while generating actions or take unexpected actions to solve problems. Additionally, reliability might be lower when interacting with niche applications or multiple applications at once. Prompt the model carefully when requesting complex tasks. 4. **Scrolling reliability:** The scroll action supports direction control (up, down, left, right) and a specified amount. In applications where scrolling doesn't take effect, keyboard alternatives such as Page Down can help. 5. **Spreadsheet interaction:** Use the fine-grained mouse control actions (`left_mouse_down`, `left_mouse_up`) and modifier-key combinations to select individual cells. Complex spreadsheet operations might still require multiple attempts. 6. **Account creation and content generation on social and communications platforms:** While Claude will visit websites, Claude's ability to create accounts or generate and share content or otherwise engage in human impersonation across social media websites and platforms is limited. This capability might be updated in the future. 7. **Vulnerabilities:** Vulnerabilities such as jailbreaking or prompt injection might persist across frontier AI systems, including the beta computer use API. In some circumstances, Claude will follow commands found in content, sometimes even when they conflict with your instructions. For example, instructions on webpages or contained in images might override your instructions or cause Claude to make mistakes. Consider the following: * Limiting computer use to trusted environments such as virtual machines or containers with minimal privileges * Avoiding giving computer use access to sensitive accounts or data without strict oversight * Informing end users of relevant risks and obtaining their consent before enabling or requesting permissions necessary for computer use features in your applications 8. **Inappropriate or illegal actions:** Under Anthropic's Terms of Service, you must not employ computer use to violate any laws or the Acceptable Use Policy. Always carefully review and verify Claude's computer use actions and logs. Do not use Claude for tasks requiring perfect precision or sensitive user information without human oversight. ## Data retention Computer use is a client-side tool. All screenshots, mouse actions, keyboard inputs, and any files involved in a session are captured and stored in your environment, not by Anthropic. Anthropic processes the screenshot images and action requests in real time as part of the API call. Retention for those API requests is governed by [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention). Because your application controls where and how computer use data is stored, computer use is ZDR eligible. For ZDR eligibility across all features, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention). ## Pricing Computer use follows the standard [tool use pricing](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview#pricing). When using the computer use tool: **System prompt overhead:** The computer use beta adds 466–499 tokens to the system prompt **Computer use tool token usage:** | Model | Input tokens per tool definition | | ----------------- | -------------------------------- | | Claude 4.x models | 735 tokens | **Additional token consumption:** * Screenshot images (see [Vision pricing](https://platform.claude.com/docs/en/build-with-claude/vision)) * Tool execution results returned to Claude If you're also using bash or text editor tools alongside computer use, those tools have their own token costs as documented in their respective pages. ## Next steps Fix the most common tool-use errors with symptom-to-fix diagnostic tables. Get started with the complete Docker-based implementation Connect Claude to external tools and APIs. See where tools execute, when Claude calls them, and which tool fits your task. Benchmarked recommendations for resolution, thinking effort, and context management --- title: Define tools url: https://platform.claude.com/docs/en/agents-and-tools/tool-use/define-tools description: Specify tool schemas, write effective descriptions, and control when Claude calls your tools. --- ## Prerequisites * Familiarity with the [tool use overview](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview) * A Claude API key and a working SDK or cURL setup ## Choosing a model Use the latest Claude Opus model, Claude Opus 5, for complex tools and ambiguous queries; it handles multiple tools better and seeks clarification when needed. Use Claude Haiku models for straightforward tools, but note they may infer missing parameters. If using Claude with tool use and thinking, see [Thinking](https://platform.claude.com/docs/en/build-with-claude/thinking) for more information. ## Specifying client tools Client tools (both Anthropic-schema and user-defined) are specified in the `tools` top-level parameter of the API request. Each tool definition includes: | Parameter | Description | | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | The name of the tool. Must match the regex `^[a-zA-Z0-9_-]{1,64}$`. | | `description` | A detailed plaintext description of what the tool does, when it should be used, and how it behaves. | | `input_schema` | A [JSON Schema](https://json-schema.org/) object defining the expected parameters for the tool. | | `input_examples` | (Optional) An array of example input objects to help Claude understand how to use the tool. See [Providing tool use examples](https://platform.claude.com/docs/en/agents-and-tools/tool-use/define-tools#providing-tool-use-examples). | For the full set of optional properties available on any tool definition, including `cache_control`, `strict`, `defer_loading`, and `allowed_callers`, see the [Tool reference](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-reference#tool-definition-properties). ```json JSON { "name": "get_weather", "description": "Get the current weather in a given location", "input_schema": { "type": "object", "properties": { "location": { "type": "string", "description": "The city and state, e.g. San Francisco, CA" }, "unit": { "type": "string", "enum": ["celsius", "fahrenheit"], "description": "The unit of temperature, either 'celsius' or 'fahrenheit'" } }, "required": ["location"] } } ``` This tool, named `get_weather`, expects an input object with a required `location` string and an optional `unit` string that must be either "celsius" or "fahrenheit". ### Tool use system prompt When you call the Claude API with the `tools` parameter, the API constructs a special system prompt from the tool definitions, tool configuration, and any user-specified system prompt. The constructed prompt is designed to instruct the model to use the specified tool(s) and provide the necessary context for the tool to operate properly: ```text wrap In this environment you have access to a set of tools you can use to answer the user's question. {{ FORMATTING INSTRUCTIONS }} String and scalar parameters should be specified as is, while lists and objects should use JSON format. Note that spaces for string values are not stripped. The output is not expected to be valid XML and is parsed with regular expressions. Here are the functions available in JSONSchema format: {{ TOOL DEFINITIONS IN JSON SCHEMA }} {{ USER SYSTEM PROMPT }} {{ TOOL CONFIGURATION }} ``` ### Best practices for tool definitions To get the best performance out of Claude when using tools, follow these guidelines: * **Provide extremely detailed descriptions.** This is by far the most important factor in tool performance. Your descriptions should explain every detail about the tool, including: * What the tool does * When it should be used (and when it shouldn't) * What each parameter means and how it affects the tool's behavior * Any important caveats or limitations, such as what information the tool does not return if the tool name is unclear. The more context you can give Claude about your tools, the better it will be at deciding when and how to use them. Aim for at least 3–4 sentences for each tool description, more if the tool is complex. * **Prioritize descriptions, but consider using `input_examples` for complex tools.** Clear descriptions are most important, but for tools with complex inputs, nested objects, or format-sensitive parameters, you can use the `input_examples` field to provide schema-validated examples. See [Providing tool use examples](https://platform.claude.com/docs/en/agents-and-tools/tool-use/define-tools#providing-tool-use-examples) for details. * **Consolidate related operations into fewer tools.** Rather than creating a separate tool for every action (`create_pr`, `review_pr`, `merge_pr`), group them into a single tool with an `action` parameter. Fewer, more capable tools reduce selection ambiguity and make your tool surface easier for Claude to navigate. * **Use meaningful namespacing in tool names.** When your tools span multiple services or resources, prefix names with the service (for example, `github_list_prs`, `slack_send_message`). This makes tool selection unambiguous as your library grows, and is especially important when using [tool search](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool). * **Design tool responses to return only high-signal information.** Return semantic, stable identifiers (for example, slugs or UUIDs) rather than opaque internal references, and include only the fields Claude needs to reason about its next step. Bloated responses waste context and make it harder for Claude to extract what matters. ```json JSON { "name": "get_stock_price", "description": "Retrieves the current stock price for a given ticker symbol. The ticker symbol must be a valid symbol for a publicly traded company on a major US stock exchange like NYSE or NASDAQ. The tool will return the latest trade price in USD. It should be used when the user asks about the current or most recent price of a specific stock. It will not provide any other information about the stock or company.", "input_schema": { "type": "object", "properties": { "ticker": { "type": "string", "description": "The stock ticker symbol, e.g. AAPL for Apple Inc." } }, "required": ["ticker"] } } ``` ```json JSON { "name": "get_stock_price", "description": "Gets the stock price for a ticker.", "input_schema": { "type": "object", "properties": { "ticker": { "type": "string" } }, "required": ["ticker"] } } ``` The good description clearly explains what the tool does, when to use it, what data it returns, and what the `ticker` parameter means. The poor description is too brief and leaves Claude with many open questions about the tool's behavior and usage. For deeper guidance on tool design (consolidation, naming, and response shaping), see [Writing tools for agents](https://www.anthropic.com/engineering/writing-tools-for-agents). ## Providing tool use examples You can provide concrete examples of valid tool inputs to help Claude understand how to use your tools more effectively. This is particularly useful for complex tools with nested objects, optional parameters, or format-sensitive inputs. ### Basic usage Add an optional `input_examples` field to your tool definition with an array of example input objects. Each example must be valid according to the tool's `input_schema`: ```bash cURL curl -sS https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d @- <<'EOF' { "model": "claude-opus-5", "max_tokens": 1024, "tools": [ { "name": "get_weather", "description": "Get the current weather in a given location", "input_schema": { "type": "object", "properties": { "location": { "type": "string", "description": "The city and state, e.g. San Francisco, CA" }, "unit": { "type": "string", "enum": ["celsius", "fahrenheit"], "description": "The unit of temperature" } }, "required": ["location"] }, "input_examples": [ {"location": "San Francisco, CA", "unit": "fahrenheit"}, {"location": "Tokyo, Japan", "unit": "celsius"}, {"location": "New York, NY"} ] } ], "messages": [ {"role": "user", "content": "What's the weather like in San Francisco?"} ] } EOF ``` ```bash CLI ant messages create <<'YAML' model: claude-opus-5 max_tokens: 1024 tools: - name: get_weather description: Get the current weather in a given location input_schema: type: object properties: location: type: string description: The city and state, e.g. San Francisco, CA unit: type: string enum: [celsius, fahrenheit] description: The unit of temperature required: [location] input_examples: - location: San Francisco, CA unit: fahrenheit - location: Tokyo, Japan unit: celsius - location: New York, NY # 'unit' is optional messages: - role: user content: What's the weather like in San Francisco? YAML ``` ```python Python client = anthropic.Anthropic() response = client.messages.create( model="claude-opus-5", max_tokens=1024, tools=[ { "name": "get_weather", "description": "Get the current weather in a given location", "input_schema": { "type": "object", "properties": { "location": { "type": "string", "description": "The city and state, e.g. San Francisco, CA", }, "unit": { "type": "string", "enum": ["celsius", "fahrenheit"], "description": "The unit of temperature", }, }, "required": ["location"], }, "input_examples": [ {"location": "San Francisco, CA", "unit": "fahrenheit"}, {"location": "Tokyo, Japan", "unit": "celsius"}, { "location": "New York, NY" # 'unit' is optional }, ], } ], messages=[{"role": "user", "content": "What's the weather like in San Francisco?"}], ) print(response) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, tools: [ { name: "get_weather", description: "Get the current weather in a given location", input_schema: { type: "object", properties: { location: { type: "string", description: "The city and state, e.g. San Francisco, CA" }, unit: { type: "string", enum: ["celsius", "fahrenheit"], description: "The unit of temperature" } }, required: ["location"] }, input_examples: [ { location: "San Francisco, CA", unit: "fahrenheit" }, { location: "Tokyo, Japan", unit: "celsius" }, { location: "New York, NY" // Demonstrates that 'unit' is optional } ] } ], messages: [{ role: "user", content: "What's the weather like in San Francisco?" }] }); console.log(response); ``` ```csharp C# AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Tools = [ new ToolUnion(new Tool() { Name = "get_weather", Description = "Get the current weather in a given location", InputSchema = new InputSchema() { Properties = new Dictionary { ["location"] = JsonSerializer.SerializeToElement(new { type = "string", description = "The city and state, e.g. San Francisco, CA" }), ["unit"] = JsonSerializer.SerializeToElement(new { type = "string", @enum = new[] { "celsius", "fahrenheit" }, description = "The unit of temperature" }), }, Required = ["location"], }, InputExamples = [ new Dictionary() { { "location", JsonSerializer.SerializeToElement("San Francisco, CA") }, { "unit", JsonSerializer.SerializeToElement("fahrenheit") }, }, new Dictionary() { { "location", JsonSerializer.SerializeToElement("Tokyo, Japan") }, { "unit", JsonSerializer.SerializeToElement("celsius") }, }, new Dictionary() { { "location", JsonSerializer.SerializeToElement("New York, NY") }, }, ], }), ], Messages = [ new() { Role = Role.User, Content = "What's the weather like in San Francisco?" } ] }; var message = await client.Messages.Create(parameters); Console.WriteLine(message); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Tools: []anthropic.ToolUnionParam{ {OfTool: &anthropic.ToolParam{ Name: "get_weather", Description: anthropic.String("Get the current weather in a given location"), InputSchema: anthropic.ToolInputSchemaParam{ Properties: map[string]any{ "location": map[string]any{ "type": "string", "description": "The city and state, e.g. San Francisco, CA", }, "unit": map[string]any{ "type": "string", "enum": []string{"celsius", "fahrenheit"}, "description": "The unit of temperature", }, }, Required: []string{"location"}, }, InputExamples: []map[string]any{ { "location": "San Francisco, CA", "unit": "fahrenheit", }, { "location": "Tokyo, Japan", "unit": "celsius", }, { "location": "New York, NY", // Demonstrates that 'unit' is optional }, }, }}, }, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("What's the weather like in San Francisco?")), }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java import com.anthropic.models.messages.Tool; import com.anthropic.models.messages.Tool.InputSchema; // ... void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addTool(Tool.builder() .name("get_weather") .description("Get the current weather in a given location") .inputSchema(InputSchema.builder() .properties(JsonValue.from(Map.of( "location", Map.of( "type", "string", "description", "The city and state, e.g. San Francisco, CA" ), "unit", Map.of( "type", "string", "enum", List.of("celsius", "fahrenheit"), "description", "The unit of temperature" ) ))) .required(List.of("location")) .build()) .putAdditionalProperty("input_examples", JsonValue.from(List.of( Map.of( "location", "San Francisco, CA", "unit", "fahrenheit" ), Map.of( "location", "Tokyo, Japan", "unit", "celsius" ), Map.of( "location", "New York, NY" ) ))) .build()) .addUserMessage("What's the weather like in San Francisco?") .build(); Message response = client.messages().create(params); IO.println(response); } ``` ```php PHP $client = new Client(); $message = $client->messages->create( maxTokens: 1024, messages: [ ['role' => 'user', 'content' => "What's the weather like in San Francisco?"] ], model: 'claude-opus-5', tools: [ [ 'name' => 'get_weather', 'description' => 'Get the current weather in a given location', 'input_schema' => [ 'type' => 'object', 'properties' => [ 'location' => [ 'type' => 'string', 'description' => 'The city and state, e.g. San Francisco, CA' ], 'unit' => [ 'type' => 'string', 'enum' => ['celsius', 'fahrenheit'], 'description' => 'The unit of temperature' ] ], 'required' => ['location'] ], 'input_examples' => [ [ 'location' => 'San Francisco, CA', 'unit' => 'fahrenheit' ], [ 'location' => 'Tokyo, Japan', 'unit' => 'celsius' ], [ 'location' => 'New York, NY' ] ] ] ], ); ``` ```ruby Ruby client = Anthropic::Client.new message = client.messages.create( model: "claude-opus-5", max_tokens: 1024, tools: [ { name: "get_weather", description: "Get the current weather in a given location", input_schema: { type: "object", properties: { location: { type: "string", description: "The city and state, e.g. San Francisco, CA" }, unit: { type: "string", enum: ["celsius", "fahrenheit"], description: "The unit of temperature" } }, required: ["location"] }, input_examples: [ { location: "San Francisco, CA", unit: "fahrenheit" }, { location: "Tokyo, Japan", unit: "celsius" }, { location: "New York, NY" } ] } ], messages: [ { role: "user", content: "What's the weather like in San Francisco?" } ] ) puts message ``` Examples are included in the prompt alongside your tool schema, showing Claude concrete patterns for well-formed tool calls. This helps Claude understand when to include optional parameters, what formats to use, and how to structure complex inputs. ### Requirements and limitations * **Schema validation** - Each example must be valid according to the tool's `input_schema`. Invalid examples return a 400 error * **Not supported for server-side tools** - Input examples work on user-defined and Anthropic-schema client tools, but not on server tools such as web search or code execution * **Token cost** - Examples add to prompt tokens: \~20–50 tokens for simple examples, \~100–200 tokens for complex nested objects ## Controlling Claude's output ### Forcing tool use In some cases, you may want Claude to use a specific tool to answer the user's question, even if Claude would otherwise answer directly without calling a tool. You can do this by specifying the tool in the `tool_choice` field of the request. The highlighted lines are the only difference from a standard tool use request: ```bash cURL curl -sS https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d @- <<'EOF' { "model": "claude-opus-5", "max_tokens": 1024, "tools": [ { "name": "get_weather", "description": "Get the current weather in a given location", "input_schema": { "type": "object", "properties": { "location": { "type": "string", "description": "The city and state, e.g. San Francisco, CA" } }, "required": ["location"] } } ], "tool_choice": {"type": "tool", "name": "get_weather"}, "messages": [ {"role": "user", "content": "What's the weather like in San Francisco?"} ] } EOF ``` ```bash CLI ant messages create <<'YAML' model: claude-opus-5 max_tokens: 1024 tools: - name: get_weather description: Get the current weather in a given location input_schema: type: object properties: location: type: string description: The city and state, e.g. San Francisco, CA required: [location] tool_choice: type: tool name: get_weather messages: - role: user content: What's the weather like in San Francisco? YAML ``` ```python Python client = anthropic.Anthropic() tools = [ { "name": "get_weather", "description": "Get the current weather in a given location", "input_schema": { "type": "object", "properties": { "location": { "type": "string", "description": "The city and state, e.g. San Francisco, CA", } }, "required": ["location"], }, } ] response = client.messages.create( model="claude-opus-5", max_tokens=1024, tools=tools, tool_choice={"type": "tool", "name": "get_weather"}, messages=[{"role": "user", "content": "What's the weather like in San Francisco?"}], ) print(response) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, tools: [ { name: "get_weather", description: "Get the current weather in a given location", input_schema: { type: "object", properties: { location: { type: "string", description: "The city and state, e.g. San Francisco, CA" } }, required: ["location"] } } ], tool_choice: { type: "tool", name: "get_weather" }, messages: [{ role: "user", content: "What's the weather like in San Francisco?" }] }); console.log(response); ``` ```csharp C# AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Tools = [ new ToolUnion(new Tool() { Name = "get_weather", Description = "Get the current weather in a given location", InputSchema = new InputSchema() { Properties = new Dictionary { ["location"] = JsonSerializer.SerializeToElement(new { type = "string", description = "The city and state, e.g. San Francisco, CA" }), }, Required = ["location"], }, }), ], ToolChoice = new ToolChoiceTool { Name = "get_weather" }, Messages = [ new() { Role = Role.User, Content = "What's the weather like in San Francisco?" } ] }; var message = await client.Messages.Create(parameters); Console.WriteLine(message); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Tools: []anthropic.ToolUnionParam{ {OfTool: &anthropic.ToolParam{ Name: "get_weather", Description: anthropic.String("Get the current weather in a given location"), InputSchema: anthropic.ToolInputSchemaParam{ Properties: map[string]any{ "location": map[string]any{ "type": "string", "description": "The city and state, e.g. San Francisco, CA", }, }, Required: []string{"location"}, }, }}, }, ToolChoice: anthropic.ToolChoiceUnionParam{OfTool: &anthropic.ToolChoiceToolParam{Name: "get_weather"}}, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("What's the weather like in San Francisco?")), }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java import com.anthropic.models.messages.Tool; import com.anthropic.models.messages.Tool.InputSchema; import com.anthropic.models.messages.ToolChoice; import com.anthropic.models.messages.ToolChoiceTool; // ... void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addTool(Tool.builder() .name("get_weather") .description("Get the current weather in a given location") .inputSchema(InputSchema.builder() .properties(JsonValue.from(Map.of( "location", Map.of( "type", "string", "description", "The city and state, e.g. San Francisco, CA" ) ))) .required(List.of("location")) .build()) .build()) .toolChoice(ToolChoice.ofTool(ToolChoiceTool.builder() .name("get_weather") .build())) .addUserMessage("What's the weather like in San Francisco?") .build(); Message response = client.messages().create(params); IO.println(response); } ``` ```php PHP $client = new Client(); $message = $client->messages->create( maxTokens: 1024, messages: [ ['role' => 'user', 'content' => "What's the weather like in San Francisco?"] ], model: 'claude-opus-5', toolChoice: ['type' => 'tool', 'name' => 'get_weather'], tools: [ [ 'name' => 'get_weather', 'description' => 'Get the current weather in a given location', 'input_schema' => [ 'type' => 'object', 'properties' => [ 'location' => [ 'type' => 'string', 'description' => 'The city and state, e.g. San Francisco, CA' ] ], 'required' => ['location'] ] ] ], ); ``` ```ruby Ruby client = Anthropic::Client.new message = client.messages.create( model: "claude-opus-5", max_tokens: 1024, tools: [ { name: "get_weather", description: "Get the current weather in a given location", input_schema: { type: "object", properties: { location: { type: "string", description: "The city and state, e.g. San Francisco, CA" } }, required: ["location"] } } ], tool_choice: { type: "tool", name: "get_weather" }, messages: [ { role: "user", content: "What's the weather like in San Francisco?" } ] ) puts message ``` When working with the `tool_choice` parameter, there are four possible options: * `auto` allows Claude to decide whether to call any provided tools or not. This is the default value when `tools` are provided. * `any` tells Claude that it must use one of the provided tools, but doesn't force a particular tool. * `tool` forces Claude to always use a particular tool. * `none` prevents Claude from using any tools. This is the default value when no `tools` are provided. When using [prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#what-invalidates-the-cache), changes to the `tool_choice` parameter will invalidate cached message blocks. Tool definitions and system prompts remain cached, but message content must be reprocessed. This diagram illustrates how each option works: ![Diagram showing the four tool_choice options: auto, any, tool, and none](https://platform.claude.com/docs/images/tool_choice.png) Note that when you have `tool_choice` as `any` or `tool`, the API prefills the assistant message to force a tool to be used. This means that the models will not emit a natural language response or explanation before `tool_use` content blocks, even if explicitly asked to do so. When using manual [extended thinking](https://platform.claude.com/docs/en/build-with-claude/extended-thinking) (`thinking: {type: "enabled"}`) with tool use, `tool_choice: {"type": "any"}` and `tool_choice: {"type": "tool", "name": "..."}` are not supported and result in an error. Only `tool_choice: {"type": "auto"}` (the default) and `tool_choice: {"type": "none"}` are compatible with manual extended thinking. [Adaptive thinking](https://platform.claude.com/docs/en/build-with-claude/thinking), including on models where thinking is on by default such as Claude Opus 5, supports forced tool use. [Claude Mythos Preview](https://anthropic.com/glasswing) does not support forced tool use. Requests with `tool_choice: {"type": "any"}` or `tool_choice: {"type": "tool", "name": "..."}` return a 400 error on this model. Use `tool_choice: {"type": "auto"}` (the default) or `tool_choice: {"type": "none"}` and rely on prompting to influence tool selection. Testing has shown that this should not reduce performance. If you would like the model to provide natural language context or explanations while still requesting that the model use a specific tool, you can use `{"type": "auto"}` for `tool_choice` (the default) and add explicit instructions in a `user` message. For example: `What's the weather like in London? Use the get_weather tool in your response.` **Guaranteed tool calls with strict tools** Combine `tool_choice: {"type": "any"}` with [strict tool use](https://platform.claude.com/docs/en/agents-and-tools/tool-use/strict-tool-use) to guarantee both that one of your tools will be called AND that the tool inputs strictly follow your schema. Set `strict: true` on your tool definitions to enable schema validation. ### Model responses with tools When using tools, Claude often comments on what it's doing or responds naturally to the user before calling tools. For example, given the prompt "What's the weather like in San Francisco right now, and what time is it there?", Claude might respond with: ```json JSON { "role": "assistant", "content": [ { "type": "text", "text": "I'll help you check the current weather and time in San Francisco." }, { "type": "tool_use", "id": "toolu_01A09q90qw90lq917835lq9", "name": "get_weather", "input": { "location": "San Francisco, CA" } } ] } ``` This natural response style helps users understand what Claude is doing and creates a more conversational interaction. You can guide the style and content of these responses through your system prompts and by providing `` in your prompts. It's important to note that Claude may use various phrasings and approaches when explaining its actions. Your code should treat these responses like any other assistant-generated text, and not rely on specific formatting conventions. ## Next steps Parse tool\_use blocks and format tool\_result responses. Let the SDK handle the agentic loop automatically. Directory of Anthropic-provided tools and optional properties. --- title: Handle tool calls url: https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls description: Parse tool_use blocks, format tool_result responses, and handle errors with is_error. --- This page covers the tool-call lifecycle: reading `tool_use` blocks from Claude's response, formatting `tool_result` blocks in your reply, and signaling errors. For the SDK abstraction that handles this automatically, see [Tool Runner](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-runner). **Simpler with Tool Runner:** The manual tool handling described on this page is automatically managed by [Tool Runner](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-runner). Use this page when you need custom control over tool execution. Claude's response differs based on whether it uses a [client or server tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview#how-tool-use-works). ## Handling results from client tools The response will have a `stop_reason` of `tool_use` and one or more `tool_use` content blocks that include: * `id`: A unique identifier for this particular tool use block. This will be used to match up the tool results later. * `name`: The name of the tool being used. * `input`: An object containing the input being passed to the tool, conforming to the tool's `input_schema`. ```json JSON { "id": "msg_01Aq9w938a90dw8q", "model": "claude-opus-5", "stop_reason": "tool_use", "role": "assistant", "content": [ { "type": "text", "text": "I'll check the current weather in San Francisco for you." }, { "type": "tool_use", "id": "toolu_01A09q90qw90lq917835lq9", "name": "get_weather", "input": { "location": "San Francisco, CA", "unit": "celsius" } } ] } ``` When you receive a tool use response for a client tool, you should: 1. Extract the `name`, `id`, and `input` from the `tool_use` block. 2. Run the actual tool in your codebase corresponding to that tool name, passing in the tool `input`. 3. Continue the conversation by sending a new message with the `role` of `user`, and a `content` block containing the `tool_result` type and the following information: * `tool_use_id`: The `id` of the tool use request this is a result for. * `content` (optional): The result of the tool, as a string (for example, `"content": "15 degrees"`), a list of nested content blocks (for example, `"content": [{"type": "text", "text": "15 degrees"}]`), or a list of document blocks (for example, `"content": [{"type": "document", "source": {"type": "text", "media_type": "text/plain", "data": "15 degrees"}}]`). These content blocks can use the `text`, `image`, `document`, or [`search_result`](https://platform.claude.com/docs/en/build-with-claude/search-results) types. * `is_error` (optional): Set to `true` if the tool execution resulted in an error. **Important formatting requirements:** * Tool result blocks must immediately follow their corresponding tool use blocks in the message history. You cannot include any messages between the assistant's tool use message and the user's tool result message. * In the user message containing tool results, the tool\_result blocks must come FIRST in the content array. Any text must come AFTER all tool results. * If the assistant turn also called a [server tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/server-tools) that has no result block yet, the user message must contain only `tool_result` blocks. Text after the results ends the turn early; for a server tool Claude called directly, the request then fails with a 400 error that names the unresolved server tool. See [Stop reasons and fallback](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons#tool-use). For example, this will cause a 400 error: ```json { "role": "user", "content": [ { "type": "text", "text": "Here are the results:" }, // ❌ Text before tool_result { "type": "tool_result", "tool_use_id": "toolu_01" /* ... */ } ] } ``` This is correct when the assistant turn calls only client tools: ```json { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_01" /* ... */ }, { "type": "text", "text": "What should I do next?" } // ✅ Text after tool_result ] } ``` If you receive an error like "tool\_use ids were found without tool\_result blocks immediately after", check that your tool results are formatted correctly. Tool results often carry content from sources outside your control: web pages, inbound email, user uploads, third-party APIs. Treat that content as untrusted: an attacker who can influence it may embed instructions that try to redirect Claude (indirect prompt injection). Keep untrusted content inside `tool_result` blocks rather than `system` prompts or plain user `text` blocks, and see [Mitigate jailbreaks and prompt injections](https://platform.claude.com/docs/en/test-and-evaluate/strengthen-guardrails/mitigate-jailbreaks#indirect-prompt-injection) for further hardening. ```json JSON { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_01A09q90qw90lq917835lq9", "content": "15 degrees" } ] } ``` ```json JSON { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_01A09q90qw90lq917835lq9", "content": [ { "type": "text", "text": "15 degrees" }, { "type": "image", "source": { "type": "base64", "media_type": "image/jpeg", "data": "/9j/4AAQSkZJRg..." } } ] } ] } ``` ```json JSON { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_01A09q90qw90lq917835lq9" } ] } ``` ```json JSON { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_01A09q90qw90lq917835lq9", "content": [ { "type": "text", "text": "The weather is" }, { "type": "document", "source": { "type": "text", "media_type": "text/plain", "data": "15 degrees" } } ] } ] } ``` After receiving the tool result, Claude will use that information to continue generating a response to the original user prompt. ## Handling results from server tools Claude executes the tool internally and incorporates the results directly into its response without requiring additional user interaction. A response can contain both a client `tool_use` block and a `server_tool_use` block that has no result block. That server tool call is not finished yet, and its result block arrives in a later response. Reply with a user message that contains only the `tool_result` blocks for the client tools and keep the same `tools` array; for a server tool Claude called directly, the API runs it on that request and the next response starts with its result block. See [Stop reasons and fallback](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons#tool-use). **Differences from other APIs** Unlike APIs that separate tool use or use special roles like `tool` or `function`, the Claude API integrates tools directly into the `user` and `assistant` message structure. Messages contain arrays of `text`, `image`, `tool_use`, and `tool_result` blocks. `user` messages include client content and `tool_result`, while `assistant` messages contain AI-generated content and `tool_use`. ## Handling errors with is\_error There are a few different types of errors that can occur when using tools with Claude: If the tool itself throws an error during execution (for example, a network error when fetching weather data), you can return the error message in the `content` along with `"is_error": true`: ```json JSON { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_01A09q90qw90lq917835lq9", "content": "ConnectionError: the weather service API is not available (HTTP 500)", "is_error": true } ] } ``` Claude will then incorporate this error into its response to the user. For example: "I'm sorry, I was unable to retrieve the current weather because the weather service API is not available. Please try again later." Write instructive error messages. Instead of generic errors like `"failed"`, include what went wrong and what Claude should try next (for example, `"Rate limit exceeded. Retry after 60 seconds."`). This gives Claude the context it needs to recover or adapt without guessing. If Claude's attempted use of a tool is invalid (for example, missing required parameters), it usually means that there wasn't enough information for Claude to use the tool correctly. Your best bet during development is to try the request again with more-detailed `description` values in your tool definitions. However, you can also continue the conversation forward with a `tool_result` that indicates the error, and Claude will try to use the tool again with the missing information filled in: ```json JSON { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_01A09q90qw90lq917835lq9", "content": "Error: Missing required 'location' parameter", "is_error": true } ] } ``` If a tool request is invalid or missing parameters, Claude will retry 2-3 times with corrections before apologizing to the user. To eliminate invalid tool calls entirely, use [strict tool use](https://platform.claude.com/docs/en/agents-and-tools/tool-use/strict-tool-use) with `strict: true` on your tool definitions. This guarantees that tool inputs will always match your schema exactly, preventing missing parameters and type mismatches. When server tools encounter errors (for example, network issues with Web Search), Claude will transparently handle these errors and attempt to provide an alternative response or explanation to the user. Unlike client tools, you do not need to handle `is_error` results for server tools. For web search specifically, possible error codes include: * `too_many_requests`: Rate limit exceeded * `invalid_input`: Invalid search query parameter * `max_uses_exceeded`: Maximum web search tool uses exceeded * `query_too_long`: Query exceeds maximum length * `unavailable`: An internal error occurred ## Next steps Handle responses where Claude calls several tools in a single turn. Let the SDK manage the `tool_use` loop, result formatting, and retries for you. Write schemas and descriptions that steer Claude toward the right tool. --- title: How tool use works url: https://platform.claude.com/docs/en/agents-and-tools/tool-use/how-tool-use-works description: Understand the tool use loop, where tools execute, and when to use tools instead of prose. --- This page explains the concepts behind tool use: where tools run, how the agentic loop works, and when tool use is the right approach. For hands-on guidance, start with the [Build a tool-using agent](https://platform.claude.com/docs/en/agents-and-tools/tool-use/build-a-tool-using-agent) tutorial or the [Define tools](https://platform.claude.com/docs/en/agents-and-tools/tool-use/define-tools) guide. ## The tool-use contract Tool use is a contract between your application and the model. You specify what operations are available and what shape their inputs and outputs take; Claude determines when and how to call them. The model never executes anything on its own. It emits a structured request, your code (or Anthropic's servers) runs the operation, and the result flows back into the conversation. This contract makes the model behave less like a text generator and more like a function you call. Engineers with classical API experience can integrate tool use the same way they would any other typed interface: define the schema, handle the callback, return a result. The difference is that the caller on the other side is a language model choosing which function to call based on the conversation. ## Where tools run The primary axis along which tools differ is where the code executes. Every tool falls into one of three buckets, and the bucket determines what your application is responsible for. ### User-defined tools (client-executed) You write the schema, you execute the code, you return the results. This is the most common case: the vast majority of tool-use traffic is [user-defined tools](https://platform.claude.com/docs/en/agents-and-tools/tool-use/define-tools) calling into application-specific logic. When Claude calls one of your tools, the API response contains a `tool_use` block with the tool name and a JSON object of arguments. Your application extracts those arguments, runs the operation (a database query, an HTTP call, a file write, whatever the tool does), and sends the output back in a `tool_result` block on the next request. Claude never sees your implementation; it only sees the schema you provided and the result you returned. ### Anthropic-schema tools (client-executed) For a handful of common operations (managing scratchpad memory, running shell commands, editing files, controlling a browser), Anthropic publishes the tool schema and your application handles execution. The tools in this category are [`memory`](https://platform.claude.com/docs/en/agents-and-tools/tool-use/memory-tool), [`bash`](https://platform.claude.com/docs/en/agents-and-tools/tool-use/bash-tool), [`text_editor`](https://platform.claude.com/docs/en/agents-and-tools/tool-use/text-editor-tool), and [`computer`](https://platform.claude.com/docs/en/agents-and-tools/tool-use/computer-use-tool). The execution model is identical to user-defined tools: the response contains a `tool_use` block, your code runs the operation, and you send back a `tool_result`. The reason to use an Anthropic-schema tool instead of defining your own equivalent is that these schemas are trained-in. Claude has been optimized on thousands of successful trajectories that use these exact tool signatures, so it calls them more reliably and recovers from errors more gracefully than it would with a custom tool that does the same thing. The schema is the interface the model already expects. ### Server-executed tools For [`web_search`](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-search-tool), [`web_fetch`](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-fetch-tool), [`code_execution`](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool), and [`tool_search`](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool), Anthropic runs the code. You enable the tool in your request and the server handles everything else. You never construct a `tool_result` block for these tools. When a turn calls only [server tools](https://platform.claude.com/docs/en/agents-and-tools/tool-use/server-tools), the server-side loop executes the operation and feeds the output back to the model before the response reaches you, unless the loop stops before it finishes, most often because it pauses. The response you receive contains `server_tool_use` blocks showing what ran and what came back. In the common case, execution is already complete by the time you see them, and your application's job is to enable the tool and read the final answer rather than to participate in the execution loop; the main exceptions are a paused loop ([`pause_turn`](https://platform.claude.com/docs/en/agents-and-tools/tool-use/how-tool-use-works#the-server-side-loop)) and a turn that also calls a client tool. ## The agentic loop (client tools) Client-executed tools (both user-defined and Anthropic-schema) require your application to drive a loop. The model can't run your code, so every tool call is a round trip: the model asks, you execute, you report back, the model continues. The canonical shape is a `while` loop keyed on `stop_reason`: 1. Send a request with your `tools` array and the user message. 2. Claude responds with `stop_reason: "tool_use"` and one or more `tool_use` blocks. 3. Execute each tool. Format the outputs as `tool_result` blocks. 4. Send a new request containing the original messages, the assistant's response, and a user message with the `tool_result` blocks. 5. Repeat from step 2 while `stop_reason` is `"tool_use"`. In practice this reads as: while `stop_reason == "tool_use"`, execute the tools and continue the conversation. The loop exits on any other stop reason (`"end_turn"`, `"max_tokens"`, `"stop_sequence"`, or `"refusal"`), which means Claude has either produced a final answer or stopped for another reason that your application should handle. For the mechanics of building requests, handling parallel tool calls, and formatting results, see [Handle tool calls](https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls). ## The server-side loop Server-executed tools run their own loop inside Anthropic's infrastructure. A single request from your application might trigger several web searches or code executions before a response comes back. The model searches, reads results, determines whether to search again, and iterates until it has what it needs, all without your application participating. This internal loop has an iteration limit. If the model is still iterating when it hits the cap, the response comes back with `stop_reason: "pause_turn"` instead of `"end_turn"`. A paused turn means the work isn't finished; re-send the conversation (including the paused response) to let the model continue where it left off. See [Server tools](https://platform.claude.com/docs/en/agents-and-tools/tool-use/server-tools) for the continuation pattern. The loop also hands control back to you before a server tool runs if Claude calls that server tool and a client tool in the same group of parallel tool calls. The response then comes back with `stop_reason: "tool_use"` and a `server_tool_use` block that has no result block yet; the API runs it after you return the client tool results. See [Stop reasons and fallback](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons#tool-use) for the exact contract. ## When to use tools (and when not to) Tool use fits when the task requires something the model can't do from text alone: * **Actions with side effects.** Sending an email, writing a file, updating a record. The model can describe these actions, but only a tool can perform them. * **Fresh or external data.** Current prices, today's weather, the contents of a database. Anything outside the training data or specific to your system needs a tool to fetch it. * **Structured, guaranteed-shape outputs.** When you need a JSON object with specific fields rather than prose that happens to contain the information, a tool schema enforces the shape. * **Calling into existing systems.** Databases, internal APIs, filesystems. Tool use is the bridge between natural-language requests and the systems that fulfill them. A clear sign that you should be using tools: if you're writing a regex to extract a decision from model output, that decision should have been a tool call. Parsing free-form text to recover structured intent is a sign the structure belongs in the schema. Tool use doesn't fit when: * The model can answer from training alone. Summarization, translation, and general-knowledge questions don't need a tool round trip. * The interaction is one-shot Q\&A with no side effects. If there's nothing to execute, there's nothing for a tool to do. * Tool-calling latency would dominate a trivial response. Every tool call is at least one extra round trip; for lightweight tasks the overhead can exceed the work. ## Choosing between approaches | Approach | When to use it | What to expect | Learn more | | ----------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | | User-defined client tools | Custom business logic, internal APIs, proprietary data | You handle execution and the agentic loop | [Define tools](https://platform.claude.com/docs/en/agents-and-tools/tool-use/define-tools) | | Anthropic-schema client tools | Standard dev operations (bash, file editing, browser control) | You handle execution; Claude calls the tool reliably because the schema is trained-in | [Tool reference](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-reference) | | Server-executed tools | Web search, code sandbox, web fetch | Anthropic handles execution; you read the results instead of producing them | [Server tools](https://platform.claude.com/docs/en/agents-and-tools/tool-use/server-tools) | ## Next steps Build an agent step by step from a single tool call to production. Schema specification, descriptions, and `tool_choice`. Directory of Anthropic-provided tools. --- title: Memory tool url: https://platform.claude.com/docs/en/agents-and-tools/tool-use/memory-tool description: Let Claude store and retrieve information across conversations by implementing the memory tool's file operations in your application. --- The memory tool lets Claude store and retrieve information across conversations in a directory of memory files. Claude can create, read, update, and delete files that persist between sessions, building up knowledge over time without keeping everything in the context window. Memory supports just-in-time context retrieval. Rather than loading all relevant information up front, an agent records what it learns in memory files and reads them back on demand. This keeps the active context focused on the current task, which matters for long-running sessions that would otherwise overwhelm the context window. See [Effective context engineering](https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents) for the broader pattern. The memory tool operates client-side: Claude requests file operations, and your application executes them. You control where and how the data is stored through your own infrastructure. Reach out through the [feedback form](https://forms.gle/YXC2EKGMhjN1c4L88) to share your feedback on this feature. For how zero data retention (ZDR) applies to this feature, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention). ## Use cases * Maintain project context across multiple agent sessions * Apply lessons from past interactions, decisions, and feedback to new tasks * Build up a knowledge base over time ## How it works When the memory tool is enabled, Claude automatically checks its memory directory before starting a task. As it works, Claude stores what it learns in files under `/memories` and reads them back in later conversations to continue earlier work. Because the memory tool is client-side, Claude only requests memory operations. Your application executes each request against storage you control and returns the result in a `tool_result` block (see [Handle tool calls](https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls)). The `/memories` path is a prefix that your handler maps onto real storage, such as a per-user directory or keys in a database. Memory lives entirely in your application. A later conversation continues from the same memory when it sends the same `tools` entry and your handler serves the same store. For security, restrict all memory operations to the `/memories` directory (see [Path traversal protection](https://platform.claude.com/docs/en/agents-and-tools/tool-use/memory-tool#path-traversal-protection)). ### Example: How memory tool calls work A typical interaction looks like this: **1. User request:** ```text wrap "Help me respond to this customer service ticket." ``` **2. Claude checks the memory directory:** ```text wrap "I'll help you respond to the customer service ticket. Let me check my memory for any previous context." ``` Claude calls the memory tool: ```json { "type": "tool_use", "id": "toolu_01C4D5E6F7G8H9I0J1K2L3M4", "name": "memory", "input": { "command": "view", "path": "/memories" } } ``` **3. Your application returns the directory contents:** ```json { "type": "tool_result", "tool_use_id": "toolu_01C4D5E6F7G8H9I0J1K2L3M4", "content": "Here're the files and directories up to 2 levels deep in /memories, excluding hidden items and node_modules:\n4.0K\t/memories\n1.5K\t/memories/customer_service_guidelines.xml\n2.0K\t/memories/refund_policies.xml" } ``` **4. Claude reads relevant files:** ```json { "type": "tool_use", "id": "toolu_01D5E6F7G8H9I0J1K2L3M4N5", "name": "memory", "input": { "command": "view", "path": "/memories/customer_service_guidelines.xml" } } ``` **5. Your application returns the file contents:** ```json { "type": "tool_result", "tool_use_id": "toolu_01D5E6F7G8H9I0J1K2L3M4N5", "content": "Here's the content of /memories/customer_service_guidelines.xml with line numbers:\n 1\t\n 2\t\n 3\t- Always address customers by their first name\n 4\t- Use empathetic language\n..." } ``` **6. Claude uses the memory to help:** ```text wrap "Based on your customer service guidelines, I can help you craft a response. Please share the ticket details..." ``` The memory tool is available on all Claude 4 and later models. For the full list of Anthropic-provided tools, see the [Tool reference](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-reference). ## Getting started The memory tool is generally available on the Messages API: no beta header is required. Using it takes two steps: 1. Add the memory tool to your request. The `tools` entry `{"type": "memory_20250818", "name": "memory"}` is the entire configuration: the `name` must be `memory`, and you don't define an input schema for an Anthropic-provided tool. 2. Implement a client-side handler for each memory command. Your handler must reject paths outside `/memories`, so read [Path traversal protection](https://platform.claude.com/docs/en/agents-and-tools/tool-use/memory-tool#path-traversal-protection) before you write it. ## Basic usage ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 2048, "messages": [ { "role": "user", "content": "Help me respond to this customer service ticket." } ], "tools": [{ "type": "memory_20250818", "name": "memory" }] }' ``` ```bash CLI ant messages create <<'YAML' model: claude-opus-5 max_tokens: 2048 tools: - type: memory_20250818 name: memory messages: - role: user content: Help me respond to this customer service ticket. YAML ``` ```python Python client = anthropic.Anthropic() message = client.messages.create( model="claude-opus-5", max_tokens=2048, messages=[ { "role": "user", "content": "Help me respond to this customer service ticket.", } ], tools=[{"type": "memory_20250818", "name": "memory"}], ) print(message) ``` ```typescript TypeScript const anthropic = new Anthropic(); const message = await anthropic.messages.create({ model: "claude-opus-5", max_tokens: 2048, messages: [ { role: "user", content: "Help me respond to this customer service ticket." } ], tools: [{ type: "memory_20250818", name: "memory" }] }); console.log(message); ``` ```csharp C# var client = new AnthropicClient(); var message = await client.Messages.Create( new() { Model = Model.ClaudeOpus5, MaxTokens = 2048, Messages = [ new() { Role = Role.User, Content = "Help me respond to this customer service ticket.", }, ], Tools = [new MemoryTool20250818()], } ); Console.WriteLine(message); ``` ```go Go client := anthropic.NewClient() message, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 2048, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Help me respond to this customer service ticket.")), }, Tools: []anthropic.ToolUnionParam{ {OfMemoryTool20250818: &anthropic.MemoryTool20250818Param{}}, }, }) if err != nil { log.Fatal(err) } fmt.Println(message) ``` ```java Java import com.anthropic.models.messages.MemoryTool20250818; // ... AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(2048L) .addTool(MemoryTool20250818.builder().build()) .addUserMessage("Help me respond to this customer service ticket.") .build(); Message message = client.messages().create(params); IO.println(message); ``` ```php PHP $client = new Client(); $message = $client->messages->create( model: Model::CLAUDE_OPUS_5, maxTokens: 2048, messages: [ [ 'role' => 'user', 'content' => 'Help me respond to this customer service ticket.', ], ], tools: [new MemoryTool20250818], ); echo $message; ``` ```ruby Ruby client = Anthropic::Client.new message = client.messages.create( model: Anthropic::Model::CLAUDE_OPUS_5, max_tokens: 2048, messages: [ { role: "user", content: "Help me respond to this customer service ticket." } ], tools: [ { type: "memory_20250818", name: "memory" } ] ) puts message ``` ## Implement the memory handler Claude's reply to a request like the previous one ends with a `tool_use` block that requests a memory operation, such as `view /memories`. Your application executes the operation and returns the result in a `tool_result` block, then sends the conversation back so Claude can continue: the standard [tool-use loop](https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls). Four SDKs provide memory tool helpers that handle the tool interface and the loop. Subclass `BetaAbstractMemoryTool` (Python and C#), use `betaMemoryTool` (TypeScript), or implement `BetaMemoryToolHandler` (Java) to back memory with your own storage, such as files on disk, a database, cloud storage, or encrypted files. Python and TypeScript also ship a ready-made local-filesystem implementation, `BetaLocalFilesystemMemoryTool`. The helper and tool-runner surfaces live in each SDK's beta namespace even though the memory tool itself is generally available. The Go and Ruby SDKs have no memory helper, so those examples run the tool-use loop themselves, and PHP wraps your handler closure in its generic `BetaRunnableTool`. All three use an in-memory store that you replace with your own storage. ```python Python import anthropic from anthropic.tools import BetaLocalFilesystemMemoryTool client = anthropic.Anthropic() memory = BetaLocalFilesystemMemoryTool(base_path="./memory") runner = client.beta.messages.tool_runner( model="claude-opus-5", max_tokens=1024, messages=[ { "role": "user", "content": "Remember that customer Acme Corp prefers email follow-ups.", } ], tools=[memory], ) final_message = runner.until_done() print(final_message.content) ``` ```typescript TypeScript import Anthropic from "@anthropic-ai/sdk"; import { betaMemoryTool } from "@anthropic-ai/sdk/helpers/beta/memory"; import { BetaLocalFilesystemMemoryTool } from "@anthropic-ai/sdk/tools/memory/node"; const client = new Anthropic(); const backend = await BetaLocalFilesystemMemoryTool.init("./memory"); const memory = betaMemoryTool(backend); // or pass your own handlers object const runner = client.beta.messages.toolRunner({ model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: "Remember that customer Acme Corp prefers email follow-ups." } ], tools: [memory], max_iterations: 10 }); const finalMessage = await runner; console.log(finalMessage.content); ``` ```csharp C# using Anthropic; using Anthropic.Helpers.Beta; using Anthropic.Models.Beta.Messages; var client = new AnthropicClient(); // Your subclass of BetaAbstractMemoryTool var memory = new FilesystemMemoryTool("./memories"); var runner = client.Beta.Messages.ToolRunner( new MessageCreateParams { Model = Anthropic.Models.Messages.Model.ClaudeOpus5, MaxTokens = 1024, Messages = [ new() { Role = Role.User, Content = "Remember that customer Acme Corp prefers email follow-ups.", }, ], }, [memory], maxIterations: 10 ); var finalMessage = await runner.RunUntilDoneAsync(); Console.WriteLine(finalMessage); ``` ```go Go package main import ( "context" "encoding/json" "fmt" "log" "slices" "sort" "strings" "github.com/anthropics/anthropic-sdk-go" ) // An in-memory store that maps memory file paths to their contents. // Use your own storage in production. var store = map[string]string{} type memoryCommand struct { Command string `json:"command"` Path string `json:"path"` FileText string `json:"file_text"` OldStr string `json:"old_str"` NewStr string `json:"new_str"` InsertLine int `json:"insert_line"` InsertText string `json:"insert_text"` OldPath string `json:"old_path"` NewPath string `json:"new_path"` } func executeMemory(raw json.RawMessage) string { var cmd memoryCommand if err := json.Unmarshal(raw, &cmd); err != nil { return "Error: invalid memory command" } switch cmd.Command { case "view": if content, ok := store[cmd.Path]; ok { lines := strings.Split(strings.TrimSuffix(content, "\n"), "\n") for i, line := range lines { lines[i] = fmt.Sprintf("%6d\t%s", i+1, line) } return fmt.Sprintf("Here's the content of %s with line numbers:\n%s", cmd.Path, strings.Join(lines, "\n")) } if cmd.Path == "/memories" { listing := []string{"1.0K\t/memories"} for path := range store { listing = append(listing, "1.0K\t"+path) } sort.Strings(listing[1:]) return fmt.Sprintf("Here're the files and directories up to 2 levels deep in %s, excluding hidden items and node_modules:\n%s", cmd.Path, strings.Join(listing, "\n")) } return fmt.Sprintf("The path %s does not exist. Please provide a valid path.", cmd.Path) case "create": store[cmd.Path] = cmd.FileText return "File created successfully at: " + cmd.Path case "str_replace": content, ok := store[cmd.Path] if !ok || !strings.Contains(content, cmd.OldStr) { return fmt.Sprintf("No replacement was performed, old_str `%s` did not appear verbatim in %s.", cmd.OldStr, cmd.Path) } store[cmd.Path] = strings.Replace(content, cmd.OldStr, cmd.NewStr, 1) return "The memory file has been edited." case "insert": content, ok := store[cmd.Path] if !ok { return fmt.Sprintf("Error: The path %s does not exist", cmd.Path) } lines := strings.Split(content, "\n") if cmd.InsertLine < 0 || cmd.InsertLine > len(lines) { return fmt.Sprintf("Error: Invalid `insert_line` parameter: %d. It should be within the range of lines of the file: [0, %d]", cmd.InsertLine, len(lines)) } lines = slices.Insert(lines, cmd.InsertLine, strings.TrimSuffix(cmd.InsertText, "\n")) store[cmd.Path] = strings.Join(lines, "\n") return fmt.Sprintf("The file %s has been edited.", cmd.Path) case "delete": if _, ok := store[cmd.Path]; !ok { return fmt.Sprintf("Error: The path %s does not exist", cmd.Path) } delete(store, cmd.Path) return "Successfully deleted " + cmd.Path case "rename": if _, ok := store[cmd.OldPath]; !ok { return fmt.Sprintf("Error: The path %s does not exist", cmd.OldPath) } if _, ok := store[cmd.NewPath]; ok { return fmt.Sprintf("Error: The destination %s already exists", cmd.NewPath) } store[cmd.NewPath] = store[cmd.OldPath] delete(store, cmd.OldPath) return fmt.Sprintf("Successfully renamed %s to %s", cmd.OldPath, cmd.NewPath) default: return "Error: unknown command " + cmd.Command } } func main() { client := anthropic.NewClient() tools := []anthropic.ToolUnionParam{{OfMemoryTool20250818: &anthropic.MemoryTool20250818Param{}}} messages := []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Remember that customer Acme Corp prefers email follow-ups.")), } for { message, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: messages, Tools: tools, }) if err != nil { log.Fatal(err) } if message.StopReason != anthropic.StopReasonToolUse { for _, block := range message.Content { if block.Type == "text" { fmt.Println(block.Text) } } break } results := []anthropic.ContentBlockParamUnion{} for _, block := range message.Content { if block.Type == "tool_use" { results = append(results, anthropic.NewToolResultBlock(block.ID, executeMemory(block.Input), false)) } } messages = append(messages, message.ToParam(), anthropic.NewUserMessage(results...)) } } ``` ```java Java import com.anthropic.client.AnthropicClient; import com.anthropic.client.okhttp.AnthropicOkHttpClient; import com.anthropic.helpers.BetaMemoryToolHandler; import com.anthropic.helpers.BetaToolRunner; import com.anthropic.models.beta.messages.BetaMemoryTool20250818; import com.anthropic.models.beta.messages.BetaMessage; import com.anthropic.models.beta.messages.MessageCreateParams; // beta package, not models.messages import com.anthropic.models.beta.messages.ToolRunnerCreateParams; import com.anthropic.models.messages.Model; import java.nio.file.Path; void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); // Your BetaMemoryToolHandler implementation of the six memory commands BetaMemoryToolHandler handler = new FileSystemMemoryToolHandler(Path.of("memories")); MessageCreateParams createParams = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addTool(BetaMemoryTool20250818.builder().build()) .addUserMessage("Remember that customer Acme Corp prefers email follow-ups.") .build(); ToolRunnerCreateParams runnerParams = ToolRunnerCreateParams.builder() .betaMemoryToolHandler(handler) .initialMessageParams(createParams) .maxIterations(10) .build(); BetaToolRunner runner = client.beta().messages().toolRunner(runnerParams); for (BetaMessage message : runner) { IO.println(message); } } ``` ```php PHP $line) { $numbered[] = sprintf("%6d\t%s", $i + 1, $line); } return "Here's the content of {$path} with line numbers:\n" . implode("\n", $numbered); } if ($path === '/memories') { $listing = ["1.0K\t/memories"]; foreach (array_keys($store) as $stored) { $listing[] = "1.0K\t{$stored}"; } return "Here're the files and directories up to 2 levels deep in {$path}, excluding hidden items and node_modules:\n" . implode("\n", $listing); } return "The path {$path} does not exist. Please provide a valid path."; case 'create': $store[$path] = $input['file_text']; return "File created successfully at: {$path}"; case 'str_replace': $position = strpos($store[$path] ?? '', $input['old_str']); if ($position === false) { return "No replacement was performed, old_str `{$input['old_str']}` did not appear verbatim in {$path}."; } $store[$path] = substr_replace($store[$path], $input['new_str'] ?? '', $position, strlen($input['old_str'])); return 'The memory file has been edited.'; case 'insert': if (!isset($store[$path])) { return "Error: The path {$path} does not exist"; } $lines = explode("\n", $store[$path]); if ($input['insert_line'] < 0 || $input['insert_line'] > count($lines)) { return "Error: Invalid `insert_line` parameter: {$input['insert_line']}. It should be within the range of lines of the file: [0, " . count($lines) . "]"; } array_splice($lines, $input['insert_line'], 0, [preg_replace('/\n\z/', '', $input['insert_text'])]); $store[$path] = implode("\n", $lines); return "The file {$path} has been edited."; case 'delete': if (!isset($store[$path])) { return "Error: The path {$path} does not exist"; } unset($store[$path]); return "Successfully deleted {$path}"; case 'rename': if (!isset($store[$input['old_path']])) { return "Error: The path {$input['old_path']} does not exist"; } if (isset($store[$input['new_path']])) { return "Error: The destination {$input['new_path']} already exists"; } $store[$input['new_path']] = $store[$input['old_path']]; unset($store[$input['old_path']]); return "Successfully renamed {$input['old_path']} to {$input['new_path']}"; default: return "Error: unknown command {$input['command']}"; } }, ); $runner = $client->beta->messages->toolRunner( maxTokens: 1024, messages: [['role' => 'user', 'content' => 'Remember that customer Acme Corp prefers email follow-ups.']], model: Model::CLAUDE_OPUS_5, tools: [$memory], maxIterations: 10, ); $finalMessage = $runner->runUntilDone(); print_r($finalMessage->content); ``` ```ruby Ruby require "anthropic" client = Anthropic::Client.new TOOLS = [{type: "memory_20250818", name: "memory"}].freeze # An in-memory store that maps memory file paths to their contents. # Use your own storage in production. STORE = {} def execute_memory(input) path = input[:path] case input[:command] when "view" if STORE.key?(path) lines = STORE[path].chomp.split("\n", -1) lines = [""] if lines.empty? numbered = lines.each_with_index.map { |line, i| format("%6d\t%s", i + 1, line) } "Here's the content of #{path} with line numbers:\n#{numbered.join("\n")}" elsif path == "/memories" listing = ["1.0K\t/memories"] + STORE.keys.map { |stored| "1.0K\t#{stored}" } "Here're the files and directories up to 2 levels deep in #{path}, excluding hidden items and node_modules:\n#{listing.join("\n")}" else "The path #{path} does not exist. Please provide a valid path." end when "create" STORE[path] = input[:file_text] "File created successfully at: #{path}" when "str_replace" unless STORE.key?(path) && STORE[path].include?(input[:old_str]) return "No replacement was performed, old_str `#{input[:old_str]}` did not appear verbatim in #{path}." end STORE[path] = STORE[path].sub(input[:old_str]) { input[:new_str].to_s } "The memory file has been edited." when "insert" return "Error: The path #{path} does not exist" unless STORE.key?(path) lines = STORE[path].split("\n", -1) lines = [""] if lines.empty? if input[:insert_line] < 0 || input[:insert_line] > lines.length return "Error: Invalid `insert_line` parameter: #{input[:insert_line]}. It should be within the range of lines of the file: [0, #{lines.length}]" end lines.insert(input[:insert_line], input[:insert_text].chomp) STORE[path] = lines.join("\n") "The file #{path} has been edited." when "delete" return "Error: The path #{path} does not exist" unless STORE.key?(path) STORE.delete(path) "Successfully deleted #{path}" when "rename" return "Error: The path #{input[:old_path]} does not exist" unless STORE.key?(input[:old_path]) return "Error: The destination #{input[:new_path]} already exists" if STORE.key?(input[:new_path]) STORE[input[:new_path]] = STORE.delete(input[:old_path]) "Successfully renamed #{input[:old_path]} to #{input[:new_path]}" else "Error: unknown command #{input[:command]}" end end messages = [{role: "user", content: "Remember that customer Acme Corp prefers email follow-ups."}] loop do message = client.messages.create( model: Anthropic::Model::CLAUDE_OPUS_5, max_tokens: 1024, messages: messages, tools: TOOLS ) unless message.stop_reason == :tool_use puts message.content break end tool_results = message.content.filter_map do |block| next unless block.type == :tool_use {type: "tool_result", tool_use_id: block.id, content: execute_memory(block.input)} end messages << {role: "assistant", content: message.content} << {role: "user", content: tool_results} end ``` The in-memory stores in the Go, PHP, and Ruby examples keep them self-contained: each one dispatches on the `command` field in the `tool_use` block's `input` and returns the strings described under [Tool commands](https://platform.claude.com/docs/en/agents-and-tools/tool-use/memory-tool#tool-commands). A production handler also needs the [path validation](https://platform.claude.com/docs/en/agents-and-tools/tool-use/memory-tool#path-traversal-protection) these demonstration stores skip. For the SDKs' own complete examples, see: * Python: [examples/memory/basic.py](https://github.com/anthropics/anthropic-sdk-python/blob/main/examples/memory/basic.py) * TypeScript: [examples/tools-helpers-memory.ts](https://github.com/anthropics/anthropic-sdk-typescript/blob/main/examples/tools-helpers-memory.ts) * C#: [MemoryToolExample](https://github.com/anthropics/anthropic-sdk-csharp/tree/main/examples/MemoryToolExample) * Java: [BetaMemoryToolExample.java](https://github.com/anthropics/anthropic-sdk-java/blob/main/anthropic-java-example/src/main/java/com/anthropic/example/BetaMemoryToolExample.java) ## Tool commands Your client-side implementation must handle the following commands. These specifications describe the recommended behaviors and return strings: Claude reads whatever text your tool result contains, so you can return different strings if your application needs to. ### view Shows directory contents or file contents with optional line ranges: ```json { "command": "view", "path": "/memories/notes.txt", "view_range": [1, 10] } ``` `view_range` is optional and applies to text-file views: `[start_line, end_line]` returns those lines, and `[start_line, -1]` returns everything from `start_line` to the end of the file. #### Return values **For directories:** Return a listing that shows files and directories with their sizes: ```text Here're the files and directories up to 2 levels deep in {path}, excluding hidden items and node_modules: {size}\t{path} {size}\t{path}/{filename1} {size}\t{path}/{filename2} ``` * Lists files up to 2 levels deep * Shows human-readable sizes (for example, `5.5K`, `1.2M`) * Excludes hidden items (files starting with `.`) and `node_modules` * Uses a tab character between the size and the path The first `view` of `/memories` on an empty store is not an error. The SDKs' local-filesystem memory tools (`BetaLocalFilesystemMemoryTool`) create the memory root before Claude's first call and return the listing header followed by a single size-and-path line for the empty directory itself. **For files:** Return file contents with a header and line numbers: ```text wrap Here's the content of {path} with line numbers: {line_numbers}{tab}{content} ``` Line number formatting: * **Width:** 6 characters, right-aligned with space padding * **Separator:** Tab character between line number and content * **Indexing:** 1-indexed (first line is line 1) * **Line limit:** Files with more than 999,999 lines should return an error: `"File {path} exceeds maximum line limit of 999,999 lines."` **Example output:** ```text Here's the content of /memories/notes.txt with line numbers: 1 Hello World 2 This is line two 10 Line ten 100 Line one hundred ``` Claude's tool description also says that `view` displays image files (`.jpg`, `.jpeg`, and `.png`) and truncates the text view of files longer than 16,000 characters. Expect `view` calls on image paths and follow-up ranged views of long files. #### Error handling * **File or directory does not exist:** `"The path {path} does not exist. Please provide a valid path."` ### create Creates a new file: ```json { "command": "create", "path": "/memories/notes.txt", "file_text": "Meeting notes:\n- Discussed project timeline\n- Next steps defined\n" } ``` #### Return values * **Success:** `"File created successfully at: {path}"` #### Error handling * **File already exists:** `"Error: File {path} already exists"` Claude's tool description says `create` "creates or overwrites" a file, so expect `create` calls on paths that already exist. Returning the error is the reference behavior, and overwriting instead is a valid implementation choice. ### str\_replace Replaces text in a file: ```json { "command": "str_replace", "path": "/memories/preferences.txt", "old_str": "Favorite color: blue", "new_str": "Favorite color: green" } ``` `new_str` is optional for `str_replace`: when it's omitted, `old_str` is deleted without a replacement. #### Return values * **Success:** `"The memory file has been edited."` followed by a snippet of the edited file with line numbers #### Error handling * **File does not exist:** `"Error: The path {path} does not exist. Please provide a valid path."` * **Text not found:** ``"No replacement was performed, old_str `\{old_str}` did not appear verbatim in {path}."`` * **Duplicate text:** When `old_str` appears multiple times, return: ``"No replacement was performed. Multiple occurrences of old_str `\{old_str}` in lines: {line_numbers}. Please ensure it is unique"`` #### Directory handling If the path is a directory, return a "file does not exist" error. ### insert Inserts text at a specific line: ```json { "command": "insert", "path": "/memories/todo.txt", "insert_line": 2, "insert_text": "- Review memory tool documentation\n" } ``` `insert_text` is inserted after line `insert_line`, and `0` inserts at the beginning of the file. #### Return values * **Success:** `"The file {path} has been edited."` #### Error handling * **File does not exist:** `"Error: The path {path} does not exist"` * **Invalid line number:** ``"Error: Invalid `insert_line` parameter: {insert_line}. It should be within the range of lines of the file: [0, {n_lines}]"`` #### Directory handling If the path is a directory, return a "file does not exist" error. ### delete Deletes a file or directory: ```json { "command": "delete", "path": "/memories/old_file.txt" } ``` #### Return values * **Success:** `"Successfully deleted {path}"` #### Error handling * **File or directory does not exist:** `"Error: The path {path} does not exist"` #### Directory handling Deletes the directory and all its contents recursively. The tool description tells Claude it cannot delete the `/memories` directory itself, so reject a `delete` whose path is the memory root. ### rename Renames or moves a file or directory: ```json { "command": "rename", "old_path": "/memories/draft.txt", "new_path": "/memories/final.txt" } ``` #### Return values * **Success:** `"Successfully renamed {old_path} to {new_path}"` #### Error handling * **Source does not exist:** `"Error: The path {old_path} does not exist"` * **Destination already exists:** Return an error (do not overwrite): `"Error: The destination {new_path} already exists"` #### Directory handling Renames the directory. The tool description tells Claude it cannot rename the `/memories` directory itself, so reject a `rename` whose `old_path` is the memory root. ## Prompting guidance When the memory tool is present in your request's `tools`, the API automatically adds this instruction to the system prompt. You don't need to send it yourself: ```text wrap IMPORTANT: ALWAYS VIEW YOUR MEMORY DIRECTORY BEFORE DOING ANYTHING ELSE. MEMORY PROTOCOL: 1. Use the `view` command of your `memory` tool to check for earlier progress. 2. ... (work on the task) ... - As you make progress, record status / progress / thoughts etc in your memory. ASSUME INTERRUPTION: Your context window might be reset at any moment, so you risk losing any progress that is not recorded in your memory directory. ``` Claude's tool description already tells it to keep the memory directory organized, so you don't need to repeat that instruction. If Claude still creates cluttered memory files, you can reinforce it in your prompt: ```text wrap Note: when editing your memory folder, always try to keep its content up-to-date, coherent and organized. You can rename or delete files that are no longer relevant. Do not create new files unless necessary. ``` You can also guide what Claude writes to memory. For example: "Only write down information relevant to \ in your memory system." ## Security considerations Your application executes every file operation Claude requests, so these safeguards are your responsibility: ### Sensitive information Claude usually refuses to write sensitive information to memory files. For stronger guarantees, add validation that strips sensitive data before your handler writes the file. ### File storage size Track memory file sizes and cap how large a file can grow. Consider capping how many characters the `view` command returns, and let Claude page through the rest with `view_range`. ### Memory expiration Periodically delete memory files that haven't been accessed in a long time. ### Path traversal protection A malicious path such as `/memories/../../secrets.env` can reach files outside the `/memories` directory. Your implementation must validate every path in every command to prevent directory traversal attacks. Consider these safeguards: * Validate that all paths start with `/memories` * Resolve paths to their canonical form and verify they remain within the memory directory * Reject paths containing sequences such as `../`, `..\\`, or other traversal patterns * Watch for URL-encoded traversal sequences (`%2e%2e%2f`) * Use your language's built-in path security utilities (for example, Python's `pathlib.Path.resolve()` and `relative_to()`) ## Error handling The memory tool uses similar error-handling patterns to the [text editor tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/text-editor-tool#handle-errors). Each command's error messages are listed under [Tool commands](https://platform.claude.com/docs/en/agents-and-tools/tool-use/memory-tool#tool-commands). To return an error to Claude, set `is_error` to `true` on the tool result and put the message in `content`: ```json { "type": "tool_result", "tool_use_id": "toolu_01C4D5E6F7G8H9I0J1K2L3M4", "content": "Error: The path /memories/notes.txt does not exist", "is_error": true } ``` ## Context editing integration The memory tool pairs with context editing to manage long-running conversations. For details, see [Context editing](https://platform.claude.com/docs/en/build-with-claude/context-editing). ## Using with compaction The memory tool can also be paired with [compaction](https://platform.claude.com/docs/en/build-with-claude/compaction), which summarizes older conversation context server-side. Context editing clears specific tool results on the client. Compaction automatically summarizes the whole conversation on the server when the conversation approaches the context window limit. For long-running agents, consider using both: compaction keeps the active context small without client-side bookkeeping, and memory preserves the information that must survive summarization. ## Multisession software development pattern For software projects that span multiple agent sessions, set up memory files deliberately instead of writing them ad hoc as work progresses. The following pattern turns memory into a recovery mechanism: each new session resumes from the state the last one recorded. ### How the pattern works 1. **Initializer session:** The first session sets up the memory files before any substantive work begins. This includes a progress log (tracking what has been done and what comes next), a feature checklist (defining the scope of work), and a reference to any startup or initialization script the project needs. 2. **Subsequent sessions:** Each new session opens by reading those memory files. This restores the project state without re-exploring the code base or retracing earlier decisions. 3. **End-of-session update:** Before a session ends, it updates the progress log with what was completed and what remains. This ensures the next session has an accurate starting point. ### Key principle Work on one feature at a time. Mark a feature complete only after end-to-end verification confirms it works, not when the code is written. This keeps the progress log accurate from session to session. For a detailed case study of this pattern in practice, including the initializer script, progress file structure, and git-based recovery, see [Effective harnesses for long-running agents](https://www.anthropic.com/engineering/effective-harnesses-for-long-running-agents). ## Next steps Execute shell commands in a persistent bash session. Automatically manage conversation context as it grows with context editing. Server-side context compaction for managing long conversations that approach context window limits. Directory of Anthropic-provided tools and reference for optional tool definition properties. --- title: Parallel tool use url: https://platform.claude.com/docs/en/agents-and-tools/tool-use/parallel-tool-use description: Enable, format, and disable parallel tool calls, with message-history guidance and troubleshooting. --- By default, Claude may call multiple tools in a single response. This page covers how to run those calls, how to format the message history so parallelism keeps working, and how to disable parallel tool use when you need to. For the single-call flow, see [Handle tool calls](https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls). ## Execution semantics When Claude calls tools, the response has a `stop_reason` of `tool_use` and can contain several `tool_use` blocks in a single assistant turn. How you run those calls is your decision. The API doesn't prescribe an execution order: you can run the calls concurrently (`Promise.all`, `asyncio.gather`), sequentially in the order they appear, or in any combination that suits your tools. Choose the strategy based on what your tools do. Independent, read-only operations are usually safe to run in parallel for lower latency. Tools with side effects, shared state, or ordering requirements might be better run sequentially. Whichever strategy you use, return one `tool_result` for each `tool_use` block, all together in the next user message. Match each result to its call with `tool_use_id`, and put every `tool_result` block before any text content in that message. See [Handle tool calls](https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls) for the full formatting rules. If you choose not to run a particular call (for example, because you ran the batch sequentially and an earlier call failed), still return a `tool_result` for it with `is_error: true` and a brief explanation. ```json { "type": "tool_result", "tool_use_id": "toolu_02", "is_error": true, "content": "Not executed: the preceding write_file call failed." } ``` ## Test parallel tool calls **Use the Tool Runner for most applications:** the SDK [Tool Runner](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-runner) handles responses with multiple tool calls and formats the results for you, so you don't write this handling yourself. Use the manual pattern on this page when you need direct control over how the calls run, such as custom batching, ordering, or error handling. The following script sends a request that should trigger parallel tool calls, verifies the response contains them, and formats the tool results so parallelism keeps working. Run it with `ANTHROPIC_API_KEY` set in your environment: ```bash cURL # This end-to-end test flow doesn't translate well to a one-off shell command. # See the SDK tabs for the full flow. The underlying HTTP request is a standard # tool use request with multiple tools defined. ``` ```bash CLI # This end-to-end test flow doesn't translate well to a one-off shell command. # See the SDK tabs for the full flow. ``` ```python Python client = Anthropic() # Define tools tools = [ { "name": "get_weather", "description": "Get the current weather in a given location", "input_schema": { "type": "object", "properties": { "location": { "type": "string", "description": "The city and state, e.g. San Francisco, CA", } }, "required": ["location"], }, }, { "name": "get_time", "description": "Get the current time in a given timezone", "input_schema": { "type": "object", "properties": { "timezone": { "type": "string", "description": "The timezone, e.g. America/New_York", } }, "required": ["timezone"], }, }, ] # Test conversation with parallel tool calls messages = [ { "role": "user", "content": "What's the weather in SF and NYC, and what time is it there?", } ] # Make initial request print("Requesting parallel tool calls...") response = client.messages.create( model="claude-opus-5", max_tokens=1024, messages=messages, tools=tools ) # Check for parallel tool calls tool_uses = [block for block in response.content if block.type == "tool_use"] print(f"\n✓ Claude made {len(tool_uses)} tool calls") if len(tool_uses) > 1: print("✓ Parallel tool calls detected!") for tool in tool_uses: print(f" - {tool.name}: {tool.input}") else: print("✗ No parallel tool calls detected") # Simulate tool execution and format results correctly tool_results = [] for tool_use in tool_uses: if tool_use.name == "get_weather": if "San Francisco" in str(tool_use.input): result = "San Francisco: 68°F, partly cloudy" else: result = "New York: 45°F, clear skies" else: # get_time if "Los_Angeles" in str(tool_use.input): result = "2:30 PM PST" else: result = "5:30 PM EST" tool_results.append( {"type": "tool_result", "tool_use_id": tool_use.id, "content": result} ) # Continue conversation with tool results messages.extend( [ {"role": "assistant", "content": response.content}, {"role": "user", "content": tool_results}, # All results in one message! ] ) # Get final response print("\nGetting final response...") final_response = client.messages.create( model="claude-opus-5", max_tokens=1024, messages=messages, tools=tools ) final_text = next( block.text for block in final_response.content if block.type == "text" ) print(f"\nClaude's response:\n{final_text}") # Verify formatting print("\n--- Verification ---") print(f"✓ Tool results sent in single user message: {len(tool_results)} results") print("✓ No text before tool results in content array") print("✓ Conversation formatted correctly for future parallel tool use") ``` ```typescript TypeScript const client = new Anthropic(); // Define tools const tools: Anthropic.Tool[] = [ { name: "get_weather", description: "Get the current weather in a given location", input_schema: { type: "object" as const, properties: { location: { type: "string", description: "The city and state, e.g. San Francisco, CA" } }, required: ["location"] } }, { name: "get_time", description: "Get the current time in a given timezone", input_schema: { type: "object" as const, properties: { timezone: { type: "string", description: "The timezone, e.g. America/New_York" } }, required: ["timezone"] } } ]; // Make initial request console.log("Requesting parallel tool calls..."); const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: "What's the weather in SF and NYC, and what time is it there?" } ], tools: tools }); // Check for parallel tool calls const toolUses = response.content.filter((block) => block.type === "tool_use"); console.log(`\n✓ Claude made ${toolUses.length} tool calls`); if (toolUses.length > 1) { console.log("✓ Parallel tool calls detected!"); for (const tool of toolUses) { if (tool.type === "tool_use") { console.log(` - ${tool.name}: ${JSON.stringify(tool.input)}`); } } } else { console.log("✗ No parallel tool calls detected"); } // Simulate tool execution and format results correctly const toolResults: Anthropic.ToolResultBlockParam[] = toolUses .filter((block): block is Anthropic.ToolUseBlock => block.type === "tool_use") .map((toolUse) => { const input = toolUse.input as Record; let result: string; if (toolUse.name === "get_weather") { result = input.location?.includes("San Francisco") ? "San Francisco: 68F, partly cloudy" : "New York: 45F, clear skies"; } else { result = input.timezone?.includes("Los_Angeles") ? "2:30 PM PST" : "5:30 PM EST"; } return { type: "tool_result" as const, tool_use_id: toolUse.id, content: result }; }); // Get final response with correct formatting console.log("\nGetting final response..."); const finalResponse = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: "What's the weather in SF and NYC, and what time is it there?" }, { role: "assistant", content: response.content }, { role: "user", content: toolResults } ], tools: tools }); for (const block of finalResponse.content) { if (block.type === "text") { console.log(`\nClaude's response:\n${block.text}`); } } // Verify formatting console.log("\n--- Verification ---"); console.log(`✓ Tool results sent in single user message: ${toolResults.length} results`); console.log("✓ No text before tool results in content array"); console.log("✓ Conversation formatted correctly for future parallel tool use"); ``` ```csharp C# AnthropicClient client = new(); var tools = new List { new ToolUnion(new Tool() { Name = "get_weather", Description = "Get the current weather in a given location", InputSchema = new InputSchema() { Properties = new Dictionary { ["location"] = JsonSerializer.SerializeToElement(new { type = "string", description = "The city and state, e.g. San Francisco, CA" }), }, Required = ["location"], }, }), new ToolUnion(new Tool() { Name = "get_time", Description = "Get the current time in a given timezone", InputSchema = new InputSchema() { Properties = new Dictionary { ["timezone"] = JsonSerializer.SerializeToElement(new { type = "string", description = "The timezone, e.g. America/New_York" }), }, Required = ["timezone"], }, }), }; Console.WriteLine("Requesting parallel tool calls..."); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [new() { Role = Role.User, Content = "What's the weather in SF and NYC, and what time is it there?" }], Tools = tools }; var response = await client.Messages.Create(parameters); var toolUses = new List(); foreach (var block in response.Content) { if (block.TryPickToolUse(out var toolUse)) { toolUses.Add(toolUse); } } Console.WriteLine($"\n✓ Claude made {toolUses.Count} tool calls"); if (toolUses.Count > 1) { Console.WriteLine("✓ Parallel tool calls detected!"); foreach (var tool in toolUses) { Console.WriteLine($" - {tool.Name}: {JsonSerializer.Serialize(tool.Input)}"); } } else { Console.WriteLine("✗ No parallel tool calls detected"); } var toolResults = new List(); foreach (var toolUse in toolUses) { string result; if (toolUse.Name == "get_weather") { result = JsonSerializer.Serialize(toolUse.Input).Contains("San Francisco") ? "San Francisco: 68°F, partly cloudy" : "New York: 45°F, clear skies"; } else { result = JsonSerializer.Serialize(toolUse.Input).Contains("Los_Angeles") ? "2:30 PM PST" : "5:30 PM EST"; } toolResults.Add(new ContentBlockParam(new ToolResultBlockParam() { ToolUseID = toolUse.ID, Content = result, })); } Console.WriteLine("\nGetting final response..."); var finalParameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [ new() { Role = Role.User, Content = "What's the weather in SF and NYC, and what time is it there?" }, new() { Role = Role.Assistant, Content = response.Content.Select(block => new ContentBlockParam(block.Json)).ToList() }, new() { Role = Role.User, Content = new MessageParamContent(toolResults) } ], Tools = tools }; var finalResponse = await client.Messages.Create(finalParameters); var text = finalResponse.Content.Select(b => b.Value).OfType().FirstOrDefault(); Console.WriteLine($"\nClaude's response:\n{text?.Text}"); Console.WriteLine("\n--- Verification ---"); Console.WriteLine($"✓ Tool results sent in single user message: {toolResults.Count} results"); Console.WriteLine("✓ No text before tool results in content array"); Console.WriteLine("✓ Conversation formatted correctly for future parallel tool use"); ``` ```go Go client := anthropic.NewClient() tools := []anthropic.ToolUnionParam{ {OfTool: &anthropic.ToolParam{ Name: "get_weather", Description: anthropic.String("Get the current weather in a given location"), InputSchema: anthropic.ToolInputSchemaParam{ Properties: map[string]any{ "location": map[string]any{ "type": "string", "description": "The city and state, e.g. San Francisco, CA", }, }, Required: []string{"location"}, }, }}, {OfTool: &anthropic.ToolParam{ Name: "get_time", Description: anthropic.String("Get the current time in a given timezone"), InputSchema: anthropic.ToolInputSchemaParam{ Properties: map[string]any{ "timezone": map[string]any{ "type": "string", "description": "The timezone, e.g. America/New_York", }, }, Required: []string{"timezone"}, }, }}, } fmt.Println("Requesting parallel tool calls...") response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("What's the weather in SF and NYC, and what time is it there?")), }, Tools: tools, }) if err != nil { log.Fatal(err) } // Find tool use blocks using type switch type toolUseInfo struct { ID string Name string Input json.RawMessage } var toolUses []toolUseInfo for _, block := range response.Content { switch variant := block.AsAny().(type) { case anthropic.ToolUseBlock: toolUses = append(toolUses, toolUseInfo{ ID: variant.ID, Name: variant.Name, Input: variant.Input, }) } } fmt.Printf("\n✓ Claude made %d tool calls\n", len(toolUses)) if len(toolUses) > 1 { fmt.Println("✓ Parallel tool calls detected!") for _, tool := range toolUses { fmt.Printf(" - %s: %s\n", tool.Name, string(tool.Input)) } } else { fmt.Println("✗ No parallel tool calls detected") } // Build tool results var toolResults []anthropic.ContentBlockParamUnion for _, toolUse := range toolUses { var result string inputStr := string(toolUse.Input) if toolUse.Name == "get_weather" { if strings.Contains(inputStr, "San Francisco") { result = "San Francisco: 68°F, partly cloudy" } else { result = "New York: 45°F, clear skies" } } else { if strings.Contains(inputStr, "Los_Angeles") { result = "2:30 PM PST" } else { result = "5:30 PM EST" } } toolResults = append(toolResults, anthropic.NewToolResultBlock(toolUse.ID, result, false)) } fmt.Println("\nGetting final response...") finalResponse, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("What's the weather in SF and NYC, and what time is it there?")), response.ToParam(), anthropic.NewUserMessage(toolResults...), }, Tools: tools, }) if err != nil { log.Fatal(err) } var finalText string for _, block := range finalResponse.Content { if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok { finalText = textBlock.Text break } } fmt.Printf("\nClaude's response:\n%s\n", finalText) fmt.Println("\n--- Verification ---") fmt.Printf("✓ Tool results sent in single user message: %d results\n", len(toolResults)) fmt.Println("✓ No text before tool results in content array") fmt.Println("✓ Conversation formatted correctly for future parallel tool use") ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); Tool weatherTool = Tool.builder() .name("get_weather") .description("Get the current weather in a given location") .inputSchema(InputSchema.builder() .properties(JsonValue.from(Map.of( "location", Map.of( "type", "string", "description", "The city and state, e.g. San Francisco, CA" ) ))) .putAdditionalProperty("required", JsonValue.from(List.of("location"))) .build()) .build(); Tool timeTool = Tool.builder() .name("get_time") .description("Get the current time in a given timezone") .inputSchema(InputSchema.builder() .properties(JsonValue.from(Map.of( "timezone", Map.of( "type", "string", "description", "The timezone, e.g. America/New_York" ) ))) .putAdditionalProperty("required", JsonValue.from(List.of("timezone"))) .build()) .build(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addTool(weatherTool) .addTool(timeTool) .addUserMessage("What's the weather in SF and NYC, and what time is it there?") .build(); IO.println("Requesting parallel tool calls..."); Message response = client.messages().create(params); List toolUses = new ArrayList<>(); for (ContentBlock block : response.content()) { if (block.toolUse().isPresent()) { toolUses.add(block.toolUse().get()); } } IO.println("\n✓ Claude made " + toolUses.size() + " tool calls"); if (toolUses.size() > 1) { IO.println("✓ Parallel tool calls detected!"); for (ToolUseBlock tool : toolUses) { IO.println(" - " + tool.name() + ": " + tool._input()); } } else { IO.println("✗ No parallel tool calls detected"); } List toolResults = new ArrayList<>(); for (ToolUseBlock toolUse : toolUses) { String result; if (toolUse.name().equals("get_weather")) { String location = toolUse._input().toString(); result = location.contains("San Francisco") ? "San Francisco: 68°F, partly cloudy" : "New York: 45°F, clear skies"; } else { String timezone = toolUse._input().toString(); result = timezone.contains("Los_Angeles") ? "2:30 PM PST" : "5:30 PM EST"; } toolResults.add(ContentBlockParam.ofToolResult( ToolResultBlockParam.builder() .toolUseId(toolUse.id()) .content(result) .build() )); } IO.println("\nGetting final response..."); MessageCreateParams finalParams = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addTool(weatherTool) .addTool(timeTool) .addUserMessage("What's the weather in SF and NYC, and what time is it there?") .addMessage(response) .addUserMessageOfBlockParams(toolResults) .build(); Message finalResponse = client.messages().create(finalParams); finalResponse.content().stream() .flatMap(block -> block.text().stream()) .forEach(textBlock -> IO.println("\nClaude's response:\n" + textBlock.text())); IO.println("\n--- Verification ---"); IO.println("✓ Tool results sent in single user message: " + toolResults.size() + " results"); IO.println("✓ No text before tool results in content array"); IO.println("✓ Conversation formatted correctly for future parallel tool use"); ``` ```php PHP $client = new Client(); $tools = [ [ 'name' => 'get_weather', 'description' => 'Get the current weather in a given location', 'input_schema' => [ 'type' => 'object', 'properties' => [ 'location' => [ 'type' => 'string', 'description' => 'The city and state, e.g. San Francisco, CA' ] ], 'required' => ['location'] ] ], [ 'name' => 'get_time', 'description' => 'Get the current time in a given timezone', 'input_schema' => [ 'type' => 'object', 'properties' => [ 'timezone' => [ 'type' => 'string', 'description' => 'The timezone, e.g. America/New_York' ] ], 'required' => ['timezone'] ] ] ]; echo "Requesting parallel tool calls...\n"; $response = $client->messages->create( maxTokens: 1024, messages: [ ['role' => 'user', 'content' => "What's the weather in SF and NYC, and what time is it there?"] ], model: 'claude-opus-5', tools: $tools, ); $toolUses = array_filter($response->content, fn($block) => $block->type === 'tool_use'); echo "\n✓ Claude made " . count($toolUses) . " tool calls\n"; if (count($toolUses) > 1) { echo "✓ Parallel tool calls detected!\n"; foreach ($toolUses as $tool) { echo " - {$tool->name}: " . json_encode($tool->input) . "\n"; } } else { echo "✗ No parallel tool calls detected\n"; } $toolResults = []; foreach ($toolUses as $toolUse) { if ($toolUse->name === 'get_weather') { $result = str_contains(json_encode($toolUse->input), 'San Francisco') ? 'San Francisco: 68°F, partly cloudy' : 'New York: 45°F, clear skies'; } else { $result = str_contains(json_encode($toolUse->input), 'Los_Angeles') ? '2:30 PM PST' : '5:30 PM EST'; } $toolResults[] = [ 'type' => 'tool_result', 'tool_use_id' => $toolUse->id, 'content' => $result ]; } echo "\nGetting final response...\n"; $finalResponse = $client->messages->create( maxTokens: 1024, messages: [ ['role' => 'user', 'content' => "What's the weather in SF and NYC, and what time is it there?"], ['role' => 'assistant', 'content' => $response->content], ['role' => 'user', 'content' => $toolResults] ], model: 'claude-opus-5', tools: $tools, ); $textBlock = array_find($finalResponse->content, static fn ($block): bool => $block->type === 'text'); echo "\nClaude's response:\n{$textBlock->text}\n"; echo "\n--- Verification ---\n"; echo "✓ Tool results sent in single user message: " . count($toolResults) . " results\n"; echo "✓ No text before tool results in content array\n"; echo "✓ Conversation formatted correctly for future parallel tool use\n"; ``` ```ruby Ruby client = Anthropic::Client.new tools = [ { name: "get_weather", description: "Get the current weather in a given location", input_schema: { type: "object", properties: { location: { type: "string", description: "The city and state, e.g. San Francisco, CA" } }, required: ["location"] } }, { name: "get_time", description: "Get the current time in a given timezone", input_schema: { type: "object", properties: { timezone: { type: "string", description: "The timezone, e.g. America/New_York" } }, required: ["timezone"] } } ] puts "Requesting parallel tool calls..." response = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: "What's the weather in SF and NYC, and what time is it there?" } ], tools: tools ) tool_uses = response.content.select { |block| block.type == :tool_use } puts "\n✓ Claude made #{tool_uses.length} tool calls" if tool_uses.length > 1 puts "✓ Parallel tool calls detected!" tool_uses.each do |tool| puts " - #{tool.name}: #{tool.input}" end else puts "✗ No parallel tool calls detected" end tool_results = tool_uses.map do |tool_use| result = if tool_use.name == "get_weather" location = tool_use.input[:location].to_s location.include?("San Francisco") ? "San Francisco: 68°F, partly cloudy" : "New York: 45°F, clear skies" else timezone = tool_use.input[:timezone].to_s timezone.include?("Los_Angeles") ? "2:30 PM PST" : "5:30 PM EST" end { type: "tool_result", tool_use_id: tool_use.id, content: result } end puts "\nGetting final response..." final_response = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: "What's the weather in SF and NYC, and what time is it there?" }, { role: "assistant", content: response.content }, { role: "user", content: tool_results } ], tools: tools ) final_text = final_response.content.find { |block| block.type == :text } puts "\nClaude's response:\n#{final_text.text}" puts "\n--- Verification ---" puts "✓ Tool results sent in single user message: #{tool_results.length} results" puts "✓ No text before tool results in content array" puts "✓ Conversation formatted correctly for future parallel tool use" ``` The summary lines at the end restate the two formatting rules that keep parallelism working: every tool result returns in a single user message, and no text content appears before the tool results in that message. ## Maximizing parallel tool use Claude 4 and later models make parallel tool calls by default when a request benefits from multiple tools. For all models, you can increase the likelihood of parallel tool calls with targeted prompting: For Claude 4 and later models, add this to your system prompt: ```text wrap For maximum efficiency, whenever you need to perform multiple independent operations, invoke all relevant tools simultaneously rather than sequentially. ``` For even stronger parallel tool use (recommended if the default isn't sufficient), use: ```text wrap For maximum efficiency, whenever you perform multiple independent operations, invoke all relevant tools simultaneously rather than sequentially. Prioritize calling tools in parallel whenever possible. For example, when reading 3 files, run 3 tool calls in parallel to read all 3 files into context at the same time. When running multiple read-only commands like `ls` or `list_dir`, always run all of the commands in parallel. Err on the side of maximizing parallel tool calls rather than running too many tools sequentially. ``` You can also encourage parallel tool use within specific user messages: ```text wrap Instead of: "What's the weather in Paris? Also check London." Use: "Check the weather in Paris and London simultaneously." Or be explicit: "Please use parallel tool calls to get the weather for Paris, London, and Tokyo at the same time." ``` ## Disable parallel tool use Parallel tool use is on by default. To turn it off, set `disable_parallel_tool_use: true` inside the [`tool_choice`](https://platform.claude.com/docs/en/agents-and-tools/tool-use/define-tools#forcing-tool-use) object. It is not a top-level request parameter. The effect depends on the `tool_choice` type. ### At most one tool call When `tool_choice` type is `auto` (the default), setting `disable_parallel_tool_use: true` means Claude calls at most one tool per response. Claude can still answer in plain text without calling any tool. The highlighted lines are the only change from a standard tool use request: ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "tools": [{ "name": "get_weather", "description": "Get the current weather in a given location", "input_schema": { "type": "object", "properties": { "location": { "type": "string", "description": "The city and state, e.g. San Francisco, CA" } }, "required": ["location"] } }], "tool_choice": {"type": "auto", "disable_parallel_tool_use": true}, "messages": [ {"role": "user", "content": "What is the weather in San Francisco and New York?"} ] }' ``` ```bash CLI ant messages create <<'YAML' model: claude-opus-5 max_tokens: 1024 tools: - name: get_weather description: Get the current weather in a given location input_schema: type: object properties: location: type: string description: The city and state, e.g. San Francisco, CA required: [location] tool_choice: type: auto disable_parallel_tool_use: true messages: - role: user content: What is the weather in San Francisco and New York? YAML ``` ```python Python client = Anthropic() response = client.messages.create( model="claude-opus-5", max_tokens=1024, tools=[ { "name": "get_weather", "description": "Get the current weather in a given location", "input_schema": { "type": "object", "properties": { "location": { "type": "string", "description": "The city and state, e.g. San Francisco, CA", } }, "required": ["location"], }, } ], tool_choice={"type": "auto", "disable_parallel_tool_use": True}, messages=[ { "role": "user", "content": "What is the weather in San Francisco and New York?", } ], ) print(response.content) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, tools: [ { name: "get_weather", description: "Get the current weather in a given location", input_schema: { type: "object", properties: { location: { type: "string", description: "The city and state, e.g. San Francisco, CA" } }, required: ["location"] } } ], tool_choice: { type: "auto", disable_parallel_tool_use: true }, messages: [{ role: "user", content: "What is the weather in San Francisco and New York?" }] }); console.log(response.content); ``` ```csharp C# AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Tools = [ new ToolUnion(new Tool() { Name = "get_weather", Description = "Get the current weather in a given location", InputSchema = new InputSchema() { Properties = new Dictionary { ["location"] = JsonSerializer.SerializeToElement(new { type = "string", description = "The city and state, e.g. San Francisco, CA" }), }, Required = ["location"], }, }), ], ToolChoice = new ToolChoiceAuto { DisableParallelToolUse = true }, Messages = [new() { Role = Role.User, Content = "What is the weather in San Francisco and New York?" }] }; var response = await client.Messages.Create(parameters); Console.WriteLine(response); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Tools: []anthropic.ToolUnionParam{ {OfTool: &anthropic.ToolParam{ Name: "get_weather", Description: anthropic.String("Get the current weather in a given location"), InputSchema: anthropic.ToolInputSchemaParam{ Properties: map[string]any{ "location": map[string]any{ "type": "string", "description": "The city and state, e.g. San Francisco, CA", }, }, Required: []string{"location"}, }, }}, }, ToolChoice: anthropic.ToolChoiceUnionParam{ OfAuto: &anthropic.ToolChoiceAutoParam{ DisableParallelToolUse: anthropic.Bool(true), }, }, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("What is the weather in San Francisco and New York?")), }, }) if err != nil { log.Fatal(err) } fmt.Println(response.Content) ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); InputSchema schema = InputSchema.builder() .properties( JsonValue.from( Map.of( "location", Map.of( "type", "string", "description", "The city and state, e.g. San Francisco, CA" ) ) ) ) .putAdditionalProperty("required", JsonValue.from(List.of("location"))) .build(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addTool( Tool.builder() .name("get_weather") .description("Get the current weather in a given location") .inputSchema(schema) .build() ) .toolChoice(ToolChoiceAuto.builder().disableParallelToolUse(true).build()) .addUserMessage("What is the weather in San Francisco and New York?") .build(); Message response = client.messages().create(params); IO.println(response.content()); ``` ```php PHP $client = new Client(); $response = $client->messages->create( maxTokens: 1024, messages: [ ['role' => 'user', 'content' => 'What is the weather in San Francisco and New York?'] ], model: 'claude-opus-5', toolChoice: ['type' => 'auto', 'disableParallelToolUse' => true], tools: [ [ 'name' => 'get_weather', 'description' => 'Get the current weather in a given location', 'input_schema' => [ 'type' => 'object', 'properties' => [ 'location' => [ 'type' => 'string', 'description' => 'The city and state, e.g. San Francisco, CA' ] ], 'required' => ['location'] ] ] ], ); echo $response; ``` ```ruby Ruby client = Anthropic::Client.new response = client.messages.create( model: "claude-opus-5", max_tokens: 1024, tools: [ { name: "get_weather", description: "Get the current weather in a given location", input_schema: { type: "object", properties: { location: { type: "string", description: "The city and state, e.g. San Francisco, CA" } }, required: ["location"] } } ], tool_choice: { type: "auto", disable_parallel_tool_use: true }, messages: [ { role: "user", content: "What is the weather in San Francisco and New York?" } ] ) puts response.content ``` ### Exactly one tool call When `tool_choice` type is `any` or `tool`, setting `disable_parallel_tool_use: true` means Claude calls exactly one tool. The following example uses `any`. The same field works with `tool`: ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "tools": [{ "name": "get_weather", "description": "Get the current weather in a given location", "input_schema": { "type": "object", "properties": { "location": { "type": "string", "description": "The city and state, e.g. San Francisco, CA" } }, "required": ["location"] } }], "tool_choice": {"type": "any", "disable_parallel_tool_use": true}, "messages": [ {"role": "user", "content": "What is the weather in San Francisco and New York?"} ] }' ``` ```bash CLI ant messages create <<'YAML' model: claude-opus-5 max_tokens: 1024 tools: - name: get_weather description: Get the current weather in a given location input_schema: type: object properties: location: type: string description: The city and state, e.g. San Francisco, CA required: [location] tool_choice: type: any disable_parallel_tool_use: true messages: - role: user content: What is the weather in San Francisco and New York? YAML ``` ```python Python client = Anthropic() response = client.messages.create( model="claude-opus-5", max_tokens=1024, tools=[ { "name": "get_weather", "description": "Get the current weather in a given location", "input_schema": { "type": "object", "properties": { "location": { "type": "string", "description": "The city and state, e.g. San Francisco, CA", } }, "required": ["location"], }, } ], tool_choice={"type": "any", "disable_parallel_tool_use": True}, messages=[ { "role": "user", "content": "What is the weather in San Francisco and New York?", } ], ) print(response.content) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, tools: [ { name: "get_weather", description: "Get the current weather in a given location", input_schema: { type: "object", properties: { location: { type: "string", description: "The city and state, e.g. San Francisco, CA" } }, required: ["location"] } } ], tool_choice: { type: "any", disable_parallel_tool_use: true }, messages: [{ role: "user", content: "What is the weather in San Francisco and New York?" }] }); console.log(response.content); ``` ```csharp C# AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Tools = [ new ToolUnion(new Tool() { Name = "get_weather", Description = "Get the current weather in a given location", InputSchema = new InputSchema() { Properties = new Dictionary { ["location"] = JsonSerializer.SerializeToElement(new { type = "string", description = "The city and state, e.g. San Francisco, CA" }), }, Required = ["location"], }, }), ], ToolChoice = new ToolChoiceAny { DisableParallelToolUse = true }, Messages = [new() { Role = Role.User, Content = "What is the weather in San Francisco and New York?" }] }; var response = await client.Messages.Create(parameters); Console.WriteLine(response); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Tools: []anthropic.ToolUnionParam{ {OfTool: &anthropic.ToolParam{ Name: "get_weather", Description: anthropic.String("Get the current weather in a given location"), InputSchema: anthropic.ToolInputSchemaParam{ Properties: map[string]any{ "location": map[string]any{ "type": "string", "description": "The city and state, e.g. San Francisco, CA", }, }, Required: []string{"location"}, }, }}, }, ToolChoice: anthropic.ToolChoiceUnionParam{ OfAny: &anthropic.ToolChoiceAnyParam{ DisableParallelToolUse: anthropic.Bool(true), }, }, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("What is the weather in San Francisco and New York?")), }, }) if err != nil { log.Fatal(err) } fmt.Println(response.Content) ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); InputSchema schema = InputSchema.builder() .properties( JsonValue.from( Map.of( "location", Map.of( "type", "string", "description", "The city and state, e.g. San Francisco, CA" ) ) ) ) .putAdditionalProperty("required", JsonValue.from(List.of("location"))) .build(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addTool( Tool.builder() .name("get_weather") .description("Get the current weather in a given location") .inputSchema(schema) .build() ) .toolChoice(ToolChoiceAny.builder().disableParallelToolUse(true).build()) .addUserMessage("What is the weather in San Francisco and New York?") .build(); Message response = client.messages().create(params); IO.println(response.content()); ``` ```php PHP $client = new Client(); $response = $client->messages->create( maxTokens: 1024, messages: [ ['role' => 'user', 'content' => 'What is the weather in San Francisco and New York?'] ], model: 'claude-opus-5', toolChoice: ['type' => 'any', 'disableParallelToolUse' => true], tools: [ [ 'name' => 'get_weather', 'description' => 'Get the current weather in a given location', 'input_schema' => [ 'type' => 'object', 'properties' => [ 'location' => [ 'type' => 'string', 'description' => 'The city and state, e.g. San Francisco, CA' ] ], 'required' => ['location'] ] ] ], ); echo $response; ``` ```ruby Ruby client = Anthropic::Client.new response = client.messages.create( model: "claude-opus-5", max_tokens: 1024, tools: [ { name: "get_weather", description: "Get the current weather in a given location", input_schema: { type: "object", properties: { location: { type: "string", description: "The city and state, e.g. San Francisco, CA" } }, required: ["location"] } } ], tool_choice: { type: "any", disable_parallel_tool_use: true }, messages: [ { role: "user", content: "What is the weather in San Francisco and New York?" } ] ) puts response.content ``` ## Troubleshooting If Claude isn't making parallel tool calls when expected, check these common issues: **1. Incorrect tool result formatting** The most common issue is formatting tool results incorrectly in the conversation history. This "teaches" Claude to avoid parallel calls. Specifically for parallel tool use: * **Wrong:** a separate user message for each tool result * **Correct:** all tool results together in a single user message ```json // Wrong: separate user messages reduce parallel tool use [ {"role": "assistant", "content": [tool_use_1, tool_use_2]}, {"role": "user", "content": [tool_result_1]}, {"role": "user", "content": [tool_result_2]} // Separate message ] // Correct: one user message with all results maintains parallel tool use [ {"role": "assistant", "content": [tool_use_1, tool_use_2]}, {"role": "user", "content": [tool_result_1, tool_result_2]} // Single message ] ``` See [Handle tool calls](https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls) for other formatting rules. **2. Weak prompting** Default prompting might not be sufficient. Use the stronger system prompt from [Maximizing parallel tool use](https://platform.claude.com/docs/en/agents-and-tools/tool-use/parallel-tool-use#maximizing-parallel-tool-use). **3. Measuring parallel tool usage** To verify parallel tool calls are working: ```bash cURL # Measuring parallel tool use is client-side analysis of responses you've already # collected, so it doesn't translate to a one-off shell command. See the SDK tabs. ``` ```bash CLI # Measuring parallel tool use is client-side analysis of responses you've already # collected, so it doesn't translate to a one-off shell command. See the SDK tabs. ``` ```python Python messages = [] # Message objects returned by client.messages.create across your run tool_call_messages = [ msg for msg in messages if any(block.type == "tool_use" for block in msg.content) ] total_tool_calls = sum( len([block for block in msg.content if block.type == "tool_use"]) for msg in tool_call_messages ) avg_tools_per_message = ( total_tool_calls / len(tool_call_messages) if tool_call_messages else 0.0 ) print(f"Average tools per message: {avg_tools_per_message}") # Should be > 1.0 if parallel calls are working ``` ```typescript TypeScript const messages: Anthropic.Message[] = []; // Message objects returned by client.messages.create across your run const toolCallMessages = messages.filter((message) => message.content.some((block) => block.type === "tool_use") ); const totalToolCalls = toolCallMessages.reduce( (sum, message) => sum + message.content.filter((block) => block.type === "tool_use").length, 0 ); const avgToolsPerMessage = toolCallMessages.length > 0 ? totalToolCalls / toolCallMessages.length : 0; console.log(`Average tools per message: ${avgToolsPerMessage}`); // Should be > 1.0 if parallel calls are working ``` ```csharp C# List messages = []; // Message objects returned by client.Messages.Create across your run var toolCallMessages = messages .Where(message => message.Content.Any(block => block.TryPickToolUse(out _))) .ToList(); var totalToolCalls = toolCallMessages .Sum(message => message.Content.Count(block => block.TryPickToolUse(out _))); var avgToolsPerMessage = toolCallMessages.Count > 0 ? (double)totalToolCalls / toolCallMessages.Count : 0.0; Console.WriteLine($"Average tools per message: {avgToolsPerMessage}"); // Should be > 1.0 if parallel calls are working ``` ```go Go var messages []anthropic.Message // Message values returned by client.Messages.New across your run toolCallMessageCount := 0 totalToolCalls := 0 for _, message := range messages { callsInMessage := 0 for _, block := range message.Content { if block.Type == "tool_use" { callsInMessage++ } } if callsInMessage > 0 { toolCallMessageCount++ totalToolCalls += callsInMessage } } avgToolsPerMessage := 0.0 if toolCallMessageCount > 0 { avgToolsPerMessage = float64(totalToolCalls) / float64(toolCallMessageCount) } fmt.Println("Average tools per message:", avgToolsPerMessage) // Should be > 1.0 if parallel calls are working ``` ```java Java List messages = List.of(); // Message objects returned by client.messages().create() across your run List toolCallMessages = messages.stream() .filter(message -> message.content().stream().anyMatch(ContentBlock::isToolUse)) .toList(); long totalToolCalls = toolCallMessages.stream() .mapToLong(message -> message.content().stream().filter(ContentBlock::isToolUse).count()) .sum(); double avgToolsPerMessage = toolCallMessages.isEmpty() ? 0.0 : (double) totalToolCalls / toolCallMessages.size(); IO.println("Average tools per message: " + avgToolsPerMessage); // Should be > 1.0 if parallel calls are working ``` ```php PHP // $messages: Message objects returned by $client->messages->create() across your run $messages = []; $toolCallMessages = array_values(array_filter( $messages, fn ($message) => count(array_filter($message->content, fn ($block) => $block->type === 'tool_use')) > 0 )); $totalToolCalls = array_sum(array_map( fn ($message) => count(array_filter($message->content, fn ($block) => $block->type === 'tool_use')), $toolCallMessages )); $avgToolsPerMessage = count($toolCallMessages) > 0 ? $totalToolCalls / count($toolCallMessages) : 0.0; echo "Average tools per message: {$avgToolsPerMessage}\n"; // Should be > 1.0 if parallel calls are working ``` ```ruby Ruby messages = [] # Message objects returned by client.messages.create across your run tool_call_messages = messages.select { |message| message.content.any? { |block| block.type == :tool_use } } total_tool_calls = tool_call_messages.sum { |message| message.content.count { |block| block.type == :tool_use } } avg_tools_per_message = tool_call_messages.empty? ? 0.0 : total_tool_calls.to_f / tool_call_messages.size puts "Average tools per message: #{avg_tools_per_message}" # Should be > 1.0 if parallel calls are working ``` **4. Calls in a batch appear to depend on each other** Execution order is your choice. If your tools have ordering dependencies, running the batch sequentially and stopping on the first failure is a valid strategy: return `is_error: true` for any call you didn't run. If you run in parallel and a call fails because its prerequisite hadn't completed, return `is_error: true` with the natural error message. Claude will reissue the call on the next turn. To reduce dependent calls appearing together, add this to your system prompt: "Only batch tool calls that are independent of each other." ## Next steps Use the SDK's Tool Runner abstraction to handle the agentic loop, error wrapping, and type safety automatically. Parse tool\_use blocks, format tool\_result responses, and handle errors with is\_error. Specify tool schemas, write effective descriptions, and control when Claude calls your tools. --- title: Server tools url: https://platform.claude.com/docs/en/agents-and-tools/tool-use/server-tools description: "Work with Anthropic-executed tools: server_tool_use blocks, pause_turn continuation, mixed server and client tool turns, and domain filtering." --- Server-executed tools share these mechanics: the `server_tool_use` block, `pause_turn` continuation, turns that mix server and client tools, Zero Data Retention (ZDR) eligibility, and domain filtering. For individual tools, see the [tool reference](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-reference). ## The server\_tool\_use block The `server_tool_use` block appears in Claude's response when a server-executed tool runs. Its `id` field uses the `srvtoolu_` prefix to distinguish it from client tool calls: ```json { "type": "server_tool_use", "id": "srvtoolu_01A2B3C4D5E6F7G8H9", "name": "web_search", "input": { "query": "latest quantum computing breakthroughs" } } ``` The API executes the tool internally. You see the call and its result in the response, but you don't handle execution. Unlike client `tool_use` blocks, you don't need to respond with a `tool_result`. The tool's result block (for example, `web_search_tool_result` for web search) follows the `server_tool_use` block in the same assistant turn, paired by `tool_use_id`. If Claude calls one of your client tools at the same time, the `server_tool_use` block appears without its result, and the response ends with `stop_reason: "tool_use"`. The API runs the tool when you return the client `tool_result` blocks in your next request. ## The server-side loop and pause\_turn When using server tools such as web search, the API executes tool calls in a server-side agentic loop. On a long-running turn, the API might pause that loop and return a `pause_turn` stop reason. Here's how to handle the `pause_turn` stop reason: ```bash cURL # Initial request. If "stop_reason" in the response is "pause_turn", continue # the turn by re-sending the request with the assistant content appended to messages. curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "messages": [ { "role": "user", "content": "Search for comprehensive information about quantum computing breakthroughs in 2025" } ], "tools": [{"type": "web_search_20250305", "name": "web_search", "max_uses": 10}] }' | jq '{stop_reason, content}' ``` ```bash CLI # Initial request. If "stop_reason" in the output is "pause_turn", re-run with # the assistant content appended to messages (see the SDK tabs). ant messages create --format json <<'YAML' | jq '{stop_reason, content}' model: claude-opus-5 max_tokens: 1024 tools: - {type: web_search_20250305, name: web_search, max_uses: 10} messages: - {role: user, content: "Search for comprehensive information about quantum computing breakthroughs in 2025"} YAML ``` ```python Python client = anthropic.Anthropic() # Initial request with web search response = client.messages.create( model="claude-opus-5", max_tokens=1024, messages=[ { "role": "user", "content": "Search for comprehensive information about quantum computing breakthroughs in 2025", } ], tools=[{"type": "web_search_20250305", "name": "web_search", "max_uses": 10}], ) # Check if the response has pause_turn stop reason if response.stop_reason == "pause_turn": # Continue the conversation with the paused content messages = [ { "role": "user", "content": "Search for comprehensive information about quantum computing breakthroughs in 2025", }, {"role": "assistant", "content": response.content}, ] # Send the continuation request continuation = client.messages.create( model="claude-opus-5", max_tokens=1024, messages=messages, tools=[{"type": "web_search_20250305", "name": "web_search", "max_uses": 10}], ) print(continuation) else: print(response) ``` ```typescript TypeScript const client = new Anthropic(); // Initial request with web search const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: "Search for comprehensive information about quantum computing breakthroughs in 2025" } ], tools: [ { type: "web_search_20250305", name: "web_search", max_uses: 10 } ] }); // Check if the response has pause_turn stop reason if (response.stop_reason === "pause_turn") { // Continue the conversation with the paused content const messages: Anthropic.MessageParam[] = [ { role: "user", content: "Search for comprehensive information about quantum computing breakthroughs in 2025" }, { role: "assistant", content: response.content } ]; // Send the continuation request const continuation = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, messages, tools: [ { type: "web_search_20250305", name: "web_search", max_uses: 10 } ] }); console.log(continuation); } else { console.log(response); } ``` ```csharp C# AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [ new() { Role = Role.User, Content = "Search for comprehensive information about quantum computing breakthroughs in 2025" } ], Tools = [new ToolUnion(new WebSearchTool20250305 { MaxUses = 10 })] }; var response = await client.Messages.Create(parameters); if (response.StopReason?.Value() == StopReason.PauseTurn) { // Continue the conversation with the paused content var continuationParams = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [ new() { Role = Role.User, Content = "Search for comprehensive information about quantum computing breakthroughs in 2025" }, new() { Role = Role.Assistant, Content = response.Content.Select(block => new ContentBlockParam(block.Json)).ToList() } ], Tools = [new ToolUnion(new WebSearchTool20250305 { MaxUses = 10 })] }; var continuation = await client.Messages.Create(continuationParams); Console.WriteLine(continuation); } else { Console.WriteLine(response); } ``` ```go Go client := anthropic.NewClient() webSearchTool := []anthropic.ToolUnionParam{ {OfWebSearchTool20250305: &anthropic.WebSearchTool20250305Param{ MaxUses: anthropic.Int(10), }}, } response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Search for comprehensive information about quantum computing breakthroughs in 2025")), }, Tools: webSearchTool, }) if err != nil { log.Fatal(err) } if response.StopReason == anthropic.StopReasonPauseTurn { // Pass the paused response back as-is so Claude can continue the turn continuation, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Search for comprehensive information about quantum computing breakthroughs in 2025")), response.ToParam(), }, Tools: webSearchTool, }) if err != nil { log.Fatal(err) } fmt.Println(continuation) } else { fmt.Println(response) } ``` ```java Java import com.anthropic.models.messages.StopReason; import com.anthropic.models.messages.WebSearchTool20250305; void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addUserMessage("Search for comprehensive information about quantum computing breakthroughs in 2025") .addTool(WebSearchTool20250305.builder() .maxUses(10L) .build()) .build(); Message response = client.messages().create(params); if (response.stopReason().isPresent() && response.stopReason().get().equals(StopReason.PAUSE_TURN)) { MessageCreateParams continuationParams = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addUserMessage("Search for comprehensive information about quantum computing breakthroughs in 2025") .addMessage(response) .addTool(WebSearchTool20250305.builder() .maxUses(10L) .build()) .build(); Message continuation = client.messages().create(continuationParams); IO.println(continuation); } else { IO.println(response); } } ``` ```php PHP $client = new Client(); $response = $client->messages->create( maxTokens: 1024, messages: [ [ 'role' => 'user', 'content' => 'Search for comprehensive information about quantum computing breakthroughs in 2025' ] ], model: 'claude-opus-5', tools: [ [ 'type' => 'web_search_20250305', 'name' => 'web_search', 'max_uses' => 10 ] ], ); if ($response->stopReason === 'pause_turn') { $messages = [ [ 'role' => 'user', 'content' => 'Search for comprehensive information about quantum computing breakthroughs in 2025' ], [ 'role' => 'assistant', 'content' => $response->content ] ]; $continuation = $client->messages->create( maxTokens: 1024, messages: $messages, model: 'claude-opus-5', tools: [ [ 'type' => 'web_search_20250305', 'name' => 'web_search', 'max_uses' => 10 ] ], ); echo $continuation; } else { echo $response; } ``` ```ruby Ruby client = Anthropic::Client.new response = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: "Search for comprehensive information about quantum computing breakthroughs in 2025" } ], tools: [ { type: "web_search_20250305", name: "web_search", max_uses: 10 } ] ) if response.stop_reason == :pause_turn messages = [ { role: "user", content: "Search for comprehensive information about quantum computing breakthroughs in 2025" }, { role: "assistant", content: response.content } ] continuation = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: messages, tools: [ { type: "web_search_20250305", name: "web_search", max_uses: 10 } ] ) puts continuation else puts response end ``` When handling `pause_turn`: * **Continue the conversation:** Pass the paused response back as-is in a subsequent request to let Claude continue its turn. * **Preserve tool state:** Include the same tools in the continuation request. A paused turn can end with a `server_tool_use` block whose tool has not run yet, and the API returns a validation error if that tool is missing from the continuation. * **Repeat as needed:** A continued turn can pause again. Check `stop_reason` on each response and continue until you get a different stop reason, capping the number of continuations as you would any retry loop. For the other `stop_reason` values and general handling patterns, see [Stop reasons and fallback](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons). ## Mixing server tools and client tools in one turn Claude can call a server tool and a client tool in the same group of parallel tool calls, for example, `web_fetch` together with a user-defined tool. A client tool is any tool that your code executes and that produces a `tool_use` block, whether it is user-defined or an Anthropic-schema client tool such as the [Bash tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/bash-tool). When that happens, the API does not run the server tool. It returns immediately so that you can run the client tool first: * `stop_reason` is `"tool_use"`, not `"pause_turn"`. * `content` contains the `server_tool_use` block and the client `tool_use` block, but no result block for the server tool: that call is not finished. * There is no other marker. Detect the state by looking for a `server_tool_use` block whose `id` has no matching result block in the response. An `mcp_tool_use` block from the [MCP connector](https://platform.claude.com/docs/en/agents-and-tools/mcp-connector) behaves the same way. Server tool calls that already have their result block in the same response are complete and need nothing from you. With [programmatic tool calling](https://platform.claude.com/docs/en/agents-and-tools/tool-use/programmatic-tool-calling), the same response shape means something different. The client `tool_use` block comes from code that is running in the `code_execution` tool rather than from Claude directly, and its `caller` field names the `code_execution` block that called it. That code has already started: it is paused waiting for your `tool_result` blocks, and sending them resumes the execution instead of starting a deferred tool. The `code_execution` block's own result block arrives once the code finishes, which can take more than one round of tool results. The follow-up user message itself is the same in both cases; with programmatic tool calling, also pass back the `id` from the response's `container` field, as that page shows. ```json { "stop_reason": "tool_use", "content": [ { "type": "text", "text": "I'll fetch the article and check your system at the same time." }, { "type": "server_tool_use", "id": "srvtoolu_01HxbWnMRmbWyMfUtJKC45rA", "name": "web_fetch", "input": { "url": "https://example.com/article" } }, { "type": "tool_use", "id": "toolu_01PjgRJLbXrXEMZwDNYLnBqk", "name": "run_command", "input": { "command": "uname -a" } } ] } ``` To continue the turn, run the client tools and send a user message whose content is only the `tool_result` blocks, one for each `tool_use` block in that response. Keep the same `tools` array: a resume request that no longer defines the waiting server tool fails with a 400 whose message ends ``but no `web_fetch` tool was provided``. ```json { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_01PjgRJLbXrXEMZwDNYLnBqk", "content": "Linux demo-host 6.8.0-52-generic x86_64 GNU/Linux" } ] } ``` The API attaches your results to the still-open assistant turn, runs the deferred server tool (for paused code execution, resumes it), and then lets Claude continue. For a server tool Claude called directly, the next response begins with the result block that answers the previous response's `server_tool_use` `id`, followed by the newly generated content and a fresh `stop_reason`: ```json { "stop_reason": "end_turn", "content": [ { "type": "web_fetch_tool_result", "tool_use_id": "srvtoolu_01HxbWnMRmbWyMfUtJKC45rA", "content": { "type": "web_fetch_result", "url": "https://example.com/article", "content": { "type": "document", "source": { "type": "text", "media_type": "text/plain", "data": "Full text content of the article..." } } } }, { "type": "text", "text": "The article argues that... and your machine is running Linux..." } ] } ``` A `server_tool_use` block and its result block pair up by `tool_use_id`, not by position: in this flow they arrive in two different responses, and the `server_tool_use` block is not repeated in the second one. On later requests, keep the whole exchange in your `messages` array in order: the first response as an `assistant` message, the `tool_result` user message, and then the next response as another `assistant` message, the same way you accumulate any other tool-use exchange. The follow-up user message must contain nothing except `tool_result` blocks. A block added after the results, such as text, tells the API that the assistant turn is over. For a server tool Claude called directly, that leaves the turn with an unresolved server tool call, and the request fails with a 400 `invalid_request_error`: ```text wrap `web_fetch` tool use with id `srvtoolu_01HxbWnMRmbWyMfUtJKC45rA` was found without a corresponding `web_fetch_tool_result` block ``` A follow-up that puts content before the results, answers only some of the client `tool_use` IDs, or contains no `tool_result` blocks at all fails earlier, with the client tool error described in [Handle tool calls](https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls): ```text wrap `tool_use` ids were found without `tool_result` blocks immediately after: toolu_01PjgRJLbXrXEMZwDNYLnBqk. Each `tool_use` block must have a corresponding `tool_result` block in the next message. ``` To give Claude more input, send it as a separate user message after the turn completes. **How this differs from `pause_turn`:** A [`pause_turn` response](https://platform.claude.com/docs/en/agents-and-tools/tool-use/server-tools#the-server-side-loop-and-pause-turn) can also end with a `server_tool_use` block that has not run, but it never leaves a client `tool_use` block waiting on you, so you continue it by re-sending the assistant content as-is. A response that leaves a client `tool_use` block waiting on you never has a `stop_reason` of `pause_turn`: when Claude stops to call your tools, `stop_reason` is `tool_use`, and you continue it by sending the client `tool_result` blocks rather than by re-sending the response. In both cases the API runs the pending server tool at the start of the next request. The following example enables web fetch together with a user-defined `run_command` tool and handles the mixed response: ```bash cURL # If "stop_reason" is "tool_use" and a server_tool_use block has no matching # result block, that call is not finished. Run the client tools, then POST # again with one more user message containing only their tool_result blocks # and the same tools array (see the SDK tabs). curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-4-8", "max_tokens": 1024, "messages": [ { "role": "user", "content": "Summarize https://example.com/article and run uname -a to tell me what system this is on." } ], "tools": [ {"type": "web_fetch_20250910", "name": "web_fetch", "max_uses": 5}, { "name": "run_command", "description": "Run a shell command on this computer and return its output.", "input_schema": { "type": "object", "properties": {"command": {"type": "string", "description": "The command to run"}}, "required": ["command"] } } ] }' | jq '{stop_reason, content}' ``` ```bash CLI # If "stop_reason" is "tool_use" and a server_tool_use block has no matching # result block, run the client tools and re-run with a user message of only # their tool_result blocks appended (see the SDK tabs). ant messages create --format json <<'YAML' | jq '{stop_reason, content}' model: claude-opus-4-8 max_tokens: 1024 messages: - role: user content: "Summarize https://example.com/article and run uname -a to tell me what system this is on." tools: - {type: web_fetch_20250910, name: web_fetch, max_uses: 5} - name: run_command description: Run a shell command on this computer and return its output. input_schema: type: object properties: command: {type: string, description: The command to run} required: [command] YAML ``` ```python Python client = anthropic.Anthropic() tools = [ {"type": "web_fetch_20250910", "name": "web_fetch", "max_uses": 5}, { "name": "run_command", "description": "Run a shell command on this computer and return its output.", "input_schema": { "type": "object", "properties": { "command": {"type": "string", "description": "The command to run"} }, "required": ["command"], }, }, ] messages = [ { "role": "user", "content": "Summarize https://example.com/article and run uname -a to tell me what system this is on.", } ] response = client.messages.create( model="claude-opus-4-8", max_tokens=1024, tools=tools, messages=messages ) tool_results = [ { "type": "tool_result", "tool_use_id": block.id, # Run your tool here. This example returns a fixed string. "content": "Linux demo-host 6.8.0-52-generic x86_64 GNU/Linux", } for block in response.content if block.type == "tool_use" ] if response.stop_reason == "tool_use" and tool_results: # A server_tool_use block with no result block in this response is not finished; its result arrives in a later response. # Send back only the client tool_result blocks, with the same tools. continuation = client.messages.create( model="claude-opus-4-8", max_tokens=1024, tools=tools, messages=[ *messages, {"role": "assistant", "content": response.content}, {"role": "user", "content": tool_results}, ], ) # If a web_fetch was deferred, it runs on this request and its # web_fetch_tool_result is the first block of continuation.content. print(continuation) else: print(response) ``` ```typescript TypeScript const client = new Anthropic(); const webFetchTool = { type: "web_fetch_20250910", name: "web_fetch", max_uses: 5 } as const; const runCommandTool: Anthropic.Tool = { name: "run_command", description: "Run a shell command on this computer and return its output.", input_schema: { type: "object" as const, properties: { command: { type: "string", description: "The command to run" } }, required: ["command"] } }; const messages: Anthropic.MessageParam[] = [ { role: "user", content: "Summarize https://example.com/article and run uname -a to tell me what system this is on." } ]; const response = await client.messages.create({ model: "claude-opus-4-8", max_tokens: 1024, tools: [webFetchTool, runCommandTool], messages }); const toolResults: Anthropic.ToolResultBlockParam[] = []; for (const block of response.content) { if (block.type === "tool_use") { toolResults.push({ type: "tool_result", tool_use_id: block.id, // Run your tool here. This example returns a fixed string. content: "Linux demo-host 6.8.0-52-generic x86_64 GNU/Linux" }); } } if (response.stop_reason === "tool_use" && toolResults.length > 0) { // A server_tool_use block with no result block in this response is not finished; its result arrives in a later response. // Send back only the client tool_result blocks, with the same tools. const continuation = await client.messages.create({ model: "claude-opus-4-8", max_tokens: 1024, tools: [webFetchTool, runCommandTool], messages: [ ...messages, { role: "assistant", content: response.content }, { role: "user", content: toolResults } ] }); // If a web_fetch was deferred, it runs on this request and its // web_fetch_tool_result is the first block of continuation.content. console.log(continuation); } else { console.log(response); } ``` ```csharp C# AnthropicClient client = new(); List tools = [ new ToolUnion(new WebFetchTool20250910() { MaxUses = 5 }), new ToolUnion(new Tool() { Name = "run_command", Description = "Run a shell command on this computer and return its output.", InputSchema = new InputSchema() { Properties = new Dictionary { ["command"] = JsonSerializer.SerializeToElement( new { type = "string", description = "The command to run" } ), }, Required = ["command"], }, }), ]; MessageParam userMessage = new() { Role = Role.User, Content = "Summarize https://example.com/article and run uname -a to tell me what system this is on." }; var response = await client.Messages.Create(new MessageCreateParams { Model = Model.ClaudeOpus4_8, MaxTokens = 1024, Tools = tools, Messages = [userMessage] }); var toolResults = new List(); foreach (var block in response.Content) { if (block.TryPickToolUse(out var toolUse)) { toolResults.Add(new ContentBlockParam(new ToolResultBlockParam() { ToolUseID = toolUse.ID, // Run your tool here. This example returns a fixed string. Content = "Linux demo-host 6.8.0-52-generic x86_64 GNU/Linux", })); } } if (response.StopReason?.Value() == StopReason.ToolUse && toolResults.Count > 0) { // A server_tool_use block with no result block in this response is not finished; its result arrives in a later response. // Send back only the client tool_result blocks, with the same tools. var continuation = await client.Messages.Create(new MessageCreateParams { Model = Model.ClaudeOpus4_8, MaxTokens = 1024, Tools = tools, Messages = [ userMessage, new() { Role = Role.Assistant, Content = response.Content.Select(block => new ContentBlockParam(block.Json)).ToList() }, new() { Role = Role.User, Content = new MessageParamContent(toolResults) } ] }); // If a web_fetch was deferred, it runs on this request and its // web_fetch_tool_result is the first block of continuation.Content. Console.WriteLine(continuation); } else { Console.WriteLine(response); } ``` ```go Go client := anthropic.NewClient() tools := []anthropic.ToolUnionParam{ {OfWebFetchTool20250910: &anthropic.WebFetchTool20250910Param{ MaxUses: anthropic.Int(5), }}, {OfTool: &anthropic.ToolParam{ Name: "run_command", Description: anthropic.String("Run a shell command on this computer and return its output."), InputSchema: anthropic.ToolInputSchemaParam{ Properties: map[string]any{ "command": map[string]any{ "type": "string", "description": "The command to run", }, }, Required: []string{"command"}, }, }}, } userMessage := anthropic.NewUserMessage(anthropic.NewTextBlock("Summarize https://example.com/article and run uname -a to tell me what system this is on.")) response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus4_8, MaxTokens: 1024, Tools: tools, Messages: []anthropic.MessageParam{userMessage}, }) if err != nil { log.Fatal(err) } var toolResults []anthropic.ContentBlockParamUnion for _, block := range response.Content { if toolUse, ok := block.AsAny().(anthropic.ToolUseBlock); ok { // Run your tool here. This example returns a fixed string. output := "Linux demo-host 6.8.0-52-generic x86_64 GNU/Linux" toolResults = append(toolResults, anthropic.NewToolResultBlock(toolUse.ID, output, false)) } } if response.StopReason == anthropic.StopReasonToolUse && len(toolResults) > 0 { // A server_tool_use block with no result block in this response is not finished; its result arrives in a later response. // Send back only the client tool_result blocks, with the same tools. continuation, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus4_8, MaxTokens: 1024, Tools: tools, Messages: []anthropic.MessageParam{ userMessage, response.ToParam(), anthropic.NewUserMessage(toolResults...), }, }) if err != nil { log.Fatal(err) } // If a web_fetch was deferred, it runs on this request and its // web_fetch_tool_result is the first block of continuation.Content. fmt.Println(continuation) } else { fmt.Println(response) } ``` ```java Java void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); Tool runCommandTool = Tool.builder() .name("run_command") .description("Run a shell command on this computer and return its output.") .inputSchema(Tool.InputSchema.builder() .properties(JsonValue.from(Map.of( "command", Map.of("type", "string", "description", "The command to run") ))) .putAdditionalProperty("required", JsonValue.from(List.of("command"))) .build()) .build(); String prompt = "Summarize https://example.com/article and run uname -a to tell me what system this is on."; Message response = client.messages().create(MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_4_8) .maxTokens(1024L) .addTool(WebFetchTool20250910.builder().maxUses(5L).build()) .addTool(runCommandTool) .addUserMessage(prompt) .build()); List toolResults = new ArrayList<>(); for (ContentBlock block : response.content()) { block.toolUse().ifPresent(toolUse -> toolResults.add(ContentBlockParam.ofToolResult( ToolResultBlockParam.builder() .toolUseId(toolUse.id()) // Run your tool here. This example returns a fixed string. .content("Linux demo-host 6.8.0-52-generic x86_64 GNU/Linux") .build() ))); } boolean isToolUse = response.stopReason() .map(StopReason.TOOL_USE::equals) .orElse(false); if (isToolUse && !toolResults.isEmpty()) { // A server_tool_use block with no result block in this response is not finished; its result arrives in a later response. // Send back only the client tool_result blocks, with the same tools. Message continuation = client.messages().create(MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_4_8) .maxTokens(1024L) .addTool(WebFetchTool20250910.builder().maxUses(5L).build()) .addTool(runCommandTool) .addUserMessage(prompt) .addMessage(response) .addUserMessageOfBlockParams(toolResults) .build()); // If a web_fetch was deferred, it runs on this request and its // web_fetch_tool_result is the first block of continuation.content(). IO.println(continuation); } else { IO.println(response); } } ``` ```php PHP $client = new Client(); $tools = [ ['type' => 'web_fetch_20250910', 'name' => 'web_fetch', 'max_uses' => 5], [ 'name' => 'run_command', 'description' => 'Run a shell command on this computer and return its output.', 'input_schema' => [ 'type' => 'object', 'properties' => [ 'command' => ['type' => 'string', 'description' => 'The command to run'] ], 'required' => ['command'] ] ] ]; $userMessage = ['role' => 'user', 'content' => 'Summarize https://example.com/article and run uname -a to tell me what system this is on.']; $response = $client->messages->create( maxTokens: 1024, messages: [$userMessage], model: 'claude-opus-4-8', tools: $tools, ); $toolResults = []; foreach ($response->content as $block) { if ($block->type === 'tool_use') { $toolResults[] = [ 'type' => 'tool_result', 'tool_use_id' => $block->id, // Run your tool here. This example returns a fixed string. 'content' => 'Linux demo-host 6.8.0-52-generic x86_64 GNU/Linux' ]; } } if ($response->stopReason === 'tool_use' && count($toolResults) > 0) { // A server_tool_use block with no result block in this response is not finished; its result arrives in a later response. // Send back only the client tool_result blocks, with the same tools. $continuation = $client->messages->create( maxTokens: 1024, messages: [ $userMessage, ['role' => 'assistant', 'content' => $response->content], ['role' => 'user', 'content' => $toolResults], ], model: 'claude-opus-4-8', tools: $tools, ); // If a web_fetch was deferred, it runs on this request and its // web_fetch_tool_result is the first block of $continuation->content. echo $continuation; } else { echo $response; } ``` ```ruby Ruby client = Anthropic::Client.new tools = [ { type: "web_fetch_20250910", name: "web_fetch", max_uses: 5 }, { name: "run_command", description: "Run a shell command on this computer and return its output.", input_schema: { type: "object", properties: { command: { type: "string", description: "The command to run" } }, required: ["command"] } } ] user_message = { role: "user", content: "Summarize https://example.com/article and run uname -a to tell me what system this is on." } response = client.messages.create( model: "claude-opus-4-8", max_tokens: 1024, tools: tools, messages: [user_message] ) tool_results = [] response.content.each do |block| next unless block.type == :tool_use tool_results << { type: "tool_result", tool_use_id: block.id, # Run your tool here. This example returns a fixed string. content: "Linux demo-host 6.8.0-52-generic x86_64 GNU/Linux" } end if response.stop_reason == :tool_use && !tool_results.empty? # A server_tool_use block with no result block in this response is not finished; its result arrives in a later response. # Send back only the client tool_result blocks, with the same tools. continuation = client.messages.create( model: "claude-opus-4-8", max_tokens: 1024, tools: tools, messages: [ user_message, { role: "assistant", content: response.content }, { role: "user", content: tool_results } ] ) # If a web_fetch was deferred, it runs on this request and its # web_fetch_tool_result is the first block of continuation.content. puts continuation else puts response end ``` This code is also correct when Claude does not mix the two kinds of call. A turn with only client `tool_use` blocks takes the same continuation path, and a turn with only server tool calls needs no client `tool_result` blocks from you: its result blocks are normally already present, and one that comes back suspended, such as a [`pause_turn` response](https://platform.claude.com/docs/en/agents-and-tools/tool-use/server-tools#the-server-side-loop-and-pause-turn), is re-sent as-is instead. ## ZDR and allowed\_callers The basic versions of web search (`web_search_20250305`) and web fetch (`web_fetch_20250910`) are eligible for [Zero Data Retention (ZDR)](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention). The `_20260209` and later versions with dynamic filtering are **not** ZDR-eligible by default because dynamic filtering relies on code execution internally. To use a `_20260209` or later server tool with ZDR, disable dynamic filtering by setting `"allowed_callers": ["direct"]` on the tool: ```json { "type": "web_search_20260209", "name": "web_search", "allowed_callers": ["direct"] } ``` This restricts the tool to direct invocation only, bypassing the internal code execution step. `allowed_callers` controls how a tool can be invoked: directly by Claude (`"direct"`), from inside a code execution container (for example, `"code_execution_20260120"`), or both. The `_20260209` versions of the web tools default to the code execution caller only; earlier versions default to `["direct"]`. On models that don't support programmatic tool calling, these versions require `allowed_callers: ["direct"]`; without it the API returns a validation error that says to set it. Even when web fetch is used in a ZDR-eligible configuration, website publishers might retain any parameters passed to the URL if Claude fetches content from their site. ## Domain filtering Server tools that access the web accept `allowed_domains` and `blocked_domains` parameters to control which domains Claude can reach. Both are fields on the tool object: ```json { "type": "web_search_20250305", "name": "web_search", "allowed_domains": ["example.com", "docs.python.org"] } ``` When using domain filters: * Domains should not include the HTTP/HTTPS scheme (use `example.com` instead of `https://example.com`). * Subdomains are automatically included (`example.com` covers `docs.example.com`). * Specific subdomains restrict results to only that subdomain (`docs.example.com` returns only results from that subdomain, not from `example.com` or `api.example.com`). * Subpaths are supported for web search and match anything after the path (`example.com/blog` matches `example.com/blog/post-1`). * Web fetch matches on the domain only: an entry that includes a path never matches a web fetch URL. * You can use either `allowed_domains` or `blocked_domains`, but not both in the same request. **Wildcard support:** * Wildcards (`*`) are not allowed in the domain itself, only in the path after it. * Valid: `example.com/*`, `example.com/*/articles` * Invalid: `*.example.com`, `ex*.com` Invalid domain formats are rejected at request time with a 400 `invalid_request_error`. Request-level domain restrictions work together with any organization-level domain restrictions configured in Claude Console. Request-level `allowed_domains` must be a subset of the organization-level allowed list; entries outside it cause the API to return a validation error. Domains your organization blocks are removed from a request-level allowed list rather than returning an error. Unicode characters in domain names can bypass domain filters through homograph attacks: `аmazon.com` (with a Cyrillic `а`) looks identical to `amazon.com` but is a different domain. Use ASCII-only domain names in allow and block lists, and audit existing entries for non-ASCII characters. ## Dynamic filtering with code execution The `_20260209` and later versions of web search and web fetch use code execution internally to apply dynamic filters against search results. You don't need to add a `code_execution` tool for these versions: when dynamic filtering runs, the API provisions code execution for the request automatically, and both tools share a single execution container. If you do include one, use `code_execution_20260120` or later; the API rejects older code execution versions alongside these web tool versions. ## Streaming server-tool events Server-tool events stream as part of the normal server-sent events (SSE) flow. A `server_tool_use` block that Claude calls directly streams like a client `tool_use` block: a `content_block_start` event followed by `input_json_delta` events. The result block arrives complete in a single `content_block_start` event, with no deltas. See [Streaming](https://platform.claude.com/docs/en/build-with-claude/streaming) for the full event reference. Individual tool pages document tool-specific event names where they differ. ## Batch requests All server tools support batch processing. In a batch, the agentic loop runs just as it does for synchronous requests, with a higher per-turn iteration limit. If the loop reaches that limit, the response ends with `stop_reason: "pause_turn"`; you can continue it by submitting a follow-up request with the returned content. See [Server tools and the agentic loop](https://platform.claude.com/docs/en/build-with-claude/batch-processing#server-tools-and-the-agentic-loop) for details. Common batch workloads include enriching a dataset with information from the web, checking a large set of documents against current sources, and running analysis code over many files. ## Next steps Fix the most common tool-use errors with symptom-to-fix diagnostic tables. Search the web and cite results. Fetch and read content from specific URLs to augment Claude's context with live web content. Run Python and bash code in a sandboxed container to analyze data, generate files, and iterate on solutions. Discover and load tools on demand. --- title: Strict tool use url: https://platform.claude.com/docs/en/agents-and-tools/tool-use/strict-tool-use description: Enforce JSON Schema compliance on Claude's tool inputs with grammar-constrained sampling. --- Setting `strict: true` on a tool definition guarantees Claude's tool inputs match your JSON Schema by constraining the model's token sampling to schema-valid outputs (a technique called grammar-constrained sampling). This page covers why strict mode matters for agents, how to enable it, and common use cases. For the supported JSON Schema subset, see [JSON Schema limitations](https://platform.claude.com/docs/en/build-with-claude/structured-outputs#json-schema-limitations). For non-strict schema guidance, see [Define tools](https://platform.claude.com/docs/en/agents-and-tools/tool-use/define-tools). Strict tool use validates tool parameters, ensuring Claude calls your functions with correctly-typed arguments. Use strict tool use when you need to: * Validate tool parameters * Build agentic workflows * Ensure type-safe function calls * Handle complex tools with nested properties ## Why strict tool use matters for agents Building reliable agentic systems requires guaranteed schema conformance. Without strict mode, Claude might return incompatible types (`"2"` instead of `2`) or omit required fields, breaking your functions and causing runtime errors. Strict tool use guarantees type-safe parameters: * Functions receive correctly-typed arguments every time * No need to validate and retry tool calls * Production-ready agents that work consistently at scale For example, suppose a booking system needs `passengers: int`. Without strict mode, Claude might provide `passengers: "two"` or `passengers: "2"`. With `strict: true`, the response always contains `passengers: 2`. ## Quick start ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "messages": [ {"role": "user", "content": "What is the weather in San Francisco?"} ], "tools": [{ "name": "get_weather", "description": "Get the current weather in a given location", "strict": true, "input_schema": { "type": "object", "properties": { "location": { "type": "string", "description": "The city and state, e.g. San Francisco, CA" }, "unit": { "type": "string", "enum": ["celsius", "fahrenheit"] } }, "required": ["location"], "additionalProperties": false } }] }' ``` ```bash CLI ant messages create --transform content <<'YAML' model: claude-opus-5 max_tokens: 1024 messages: - role: user content: What is the weather in San Francisco? tools: - name: get_weather description: Get the current weather in a given location strict: true input_schema: type: object properties: location: type: string description: The city and state, e.g. San Francisco, CA unit: type: string enum: [celsius, fahrenheit] required: [location] additionalProperties: false YAML ``` ```python Python client = anthropic.Anthropic() response = client.messages.create( model="claude-opus-5", max_tokens=1024, messages=[{"role": "user", "content": "What's the weather like in San Francisco?"}], tools=[ { "name": "get_weather", "description": "Get the current weather in a given location", "strict": True, # Enable strict mode "input_schema": { "type": "object", "properties": { "location": { "type": "string", "description": "The city and state, e.g. San Francisco, CA", }, "unit": { "type": "string", "enum": ["celsius", "fahrenheit"], "description": "The unit of temperature, either 'celsius' or 'fahrenheit'", }, }, "required": ["location"], "additionalProperties": False, }, } ], ) print(response.content) ``` ```typescript TypeScript const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }); const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: "What's the weather like in San Francisco?" } ], tools: [ { name: "get_weather", description: "Get the current weather in a given location", strict: true, // Enable strict mode input_schema: { type: "object", properties: { location: { type: "string", description: "The city and state, e.g. San Francisco, CA" }, unit: { type: "string", enum: ["celsius", "fahrenheit"] } }, required: ["location"], additionalProperties: false } } ] }); console.log(response.content); ``` ```csharp C# using System.Text.Json; using Anthropic; using Anthropic.Models.Messages; AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [new() { Role = Role.User, Content = "What's the weather like in San Francisco?" }], Tools = [ new ToolUnion(new Tool() { Name = "get_weather", Description = "Get the current weather in a given location", Strict = true, InputSchema = new InputSchema(new Dictionary { ["properties"] = JsonSerializer.SerializeToElement(new Dictionary { ["location"] = new { type = "string", description = "The city and state, e.g. San Francisco, CA" }, ["unit"] = new { type = "string", @enum = new[] { "celsius", "fahrenheit" } }, }), ["required"] = JsonSerializer.SerializeToElement(new[] { "location" }), ["additionalProperties"] = JsonSerializer.SerializeToElement(false), }), }), ] }; var message = await client.Messages.Create(parameters); Console.WriteLine(message); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("What's the weather like in San Francisco?")), }, Tools: []anthropic.ToolUnionParam{ {OfTool: &anthropic.ToolParam{ Name: "get_weather", Description: anthropic.String("Get the current weather in a given location"), Strict: anthropic.Bool(true), InputSchema: anthropic.ToolInputSchemaParam{ Properties: map[string]any{ "location": map[string]any{ "type": "string", "description": "The city and state, e.g. San Francisco, CA", }, "unit": map[string]any{ "type": "string", "enum": []string{"celsius", "fahrenheit"}, }, }, Required: []string{"location"}, ExtraFields: map[string]any{ "additionalProperties": false, }, }}}, }, }) if err != nil { log.Fatal(err) } fmt.Println(response.Content) ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); InputSchema schema = InputSchema.builder() .properties( JsonValue.from( Map.of( "location", Map.of( "type", "string", "description", "The city and state, e.g. San Francisco, CA" ), "unit", Map.of( "type", "string", "enum", List.of("celsius", "fahrenheit") ) ) ) ) .putAdditionalProperty("required", JsonValue.from(List.of("location"))) .putAdditionalProperty("additionalProperties", JsonValue.from(false)) .build(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addUserMessage("What's the weather like in San Francisco?") .addTool( Tool.builder() .name("get_weather") .description("Get the current weather in a given location") .strict(true) .inputSchema(schema) .build() ) .build(); Message response = client.messages().create(params); IO.println(response.content()); ``` ```php PHP $client = new Client(); $message = $client->messages->create( maxTokens: 1024, messages: [ ['role' => 'user', 'content' => "What's the weather like in San Francisco?"] ], model: 'claude-opus-5', tools: [ [ 'name' => 'get_weather', 'description' => 'Get the current weather in a given location', 'strict' => true, 'input_schema' => [ 'type' => 'object', 'properties' => [ 'location' => [ 'type' => 'string', 'description' => 'The city and state, e.g. San Francisco, CA' ], 'unit' => [ 'type' => 'string', 'enum' => ['celsius', 'fahrenheit'] ] ], 'required' => ['location'], 'additionalProperties' => false ] ] ], ); echo $message; ``` ```ruby Ruby client = Anthropic::Client.new message = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: "What's the weather like in San Francisco?" } ], tools: [ { name: "get_weather", description: "Get the current weather in a given location", strict: true, input_schema: { type: "object", properties: { location: { type: "string", description: "The city and state, e.g. San Francisco, CA" }, unit: { type: "string", enum: ["celsius", "fahrenheit"] } }, required: ["location"], additionalProperties: false } } ] ) puts message.content ``` **Response format:** Tool use blocks with validated inputs in `response.content[x].input` ```json Output { "type": "tool_use", "name": "get_weather", "input": { "location": "San Francisco, CA" } } ``` **Guarantees:** * Tool `input` strictly follows the `input_schema` * Tool `name` is always valid (from provided tools or server tools) ## How it works Create a JSON schema for your tool's `input_schema`. The schema uses standard JSON Schema format with some limitations (see [JSON Schema limitations](https://platform.claude.com/docs/en/build-with-claude/structured-outputs#json-schema-limitations)). Set `"strict": true` as a top-level property in your tool definition, alongside `name`, `description`, and `input_schema`. When Claude uses the tool, the `input` field in the `tool_use` block strictly follows your `input_schema`, and the `name` is always valid. ## Common use cases Ensure tool parameters exactly match your schema: ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "messages": [ {"role": "user", "content": "Search for flights to Tokyo departing June 1, 2026"} ], "tools": [{ "name": "search_flights", "strict": true, "input_schema": { "type": "object", "properties": { "destination": {"type": "string"}, "departure_date": {"type": "string", "format": "date"}, "passengers": {"type": "integer", "enum": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]} }, "required": ["destination", "departure_date"], "additionalProperties": false } }] }' ``` ```bash CLI ant messages create <<'YAML' model: claude-opus-5 max_tokens: 1024 messages: - role: user content: Search for flights to Tokyo departing June 1, 2026 tools: - name: search_flights strict: true input_schema: type: object properties: destination: type: string departure_date: type: string format: date passengers: type: integer enum: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] required: [destination, departure_date] additionalProperties: false YAML ``` ```python Python client = Anthropic() response = client.messages.create( model="claude-opus-5", max_tokens=1024, messages=[ { "role": "user", "content": "Search for flights to Tokyo departing June 1, 2026", } ], tools=[ { "name": "search_flights", "strict": True, "input_schema": { "type": "object", "properties": { "destination": {"type": "string"}, "departure_date": {"type": "string", "format": "date"}, "passengers": { "type": "integer", "enum": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], }, }, "required": ["destination", "departure_date"], "additionalProperties": False, }, } ], ) print(response) ``` ```typescript TypeScript const client = new Anthropic(); const searchFlightsTool: Anthropic.Tool = { name: "search_flights", strict: true, input_schema: { type: "object", properties: { destination: { type: "string" }, departure_date: { type: "string", format: "date" }, passengers: { type: "integer", enum: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] } }, required: ["destination", "departure_date"], additionalProperties: false } }; const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, messages: [{ role: "user", content: "Search for flights to Tokyo departing June 1, 2026" }], tools: [searchFlightsTool] }); console.log(response); ``` ```csharp C# using System.Text.Json; using Anthropic; using Anthropic.Models.Messages; AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [new() { Role = Role.User, Content = "Search for flights to Tokyo departing June 1, 2026" }], Tools = [ new ToolUnion(new Tool() { Name = "search_flights", Strict = true, InputSchema = new InputSchema(new Dictionary { ["properties"] = JsonSerializer.SerializeToElement(new Dictionary { ["destination"] = new { type = "string" }, ["departure_date"] = new { type = "string", format = "date" }, ["passengers"] = new { type = "integer", @enum = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 } }, }), ["required"] = JsonSerializer.SerializeToElement(new[] { "destination", "departure_date" }), ["additionalProperties"] = JsonSerializer.SerializeToElement(false), }), }), ] }; var message = await client.Messages.Create(parameters); Console.WriteLine(message); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Search for flights to Tokyo departing June 1, 2026")), }, Tools: []anthropic.ToolUnionParam{ {OfTool: &anthropic.ToolParam{ Name: "search_flights", Strict: anthropic.Bool(true), InputSchema: anthropic.ToolInputSchemaParam{ Properties: map[string]any{ "destination": map[string]any{ "type": "string", }, "departure_date": map[string]any{ "type": "string", "format": "date", }, "passengers": map[string]any{ "type": "integer", "enum": []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, }, }, Required: []string{"destination", "departure_date"}, ExtraFields: map[string]any{ "additionalProperties": false, }, }}}, }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); InputSchema schema = InputSchema.builder() .properties( JsonValue.from( Map.of( "destination", Map.of("type", "string"), "departure_date", Map.of("type", "string", "format", "date"), "passengers", Map.of( "type", "integer", "enum", List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) ) ) ) ) .putAdditionalProperty("required", JsonValue.from(List.of("destination", "departure_date"))) .putAdditionalProperty("additionalProperties", JsonValue.from(false)) .build(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addUserMessage("Search for flights to Tokyo departing June 1, 2026") .addTool( Tool.builder() .name("search_flights") .strict(true) .inputSchema(schema) .build() ) .build(); Message response = client.messages().create(params); IO.println(response); ``` ```php PHP $client = new Client(); $message = $client->messages->create( maxTokens: 1024, messages: [ ['role' => 'user', 'content' => 'Search for flights to Tokyo departing June 1, 2026'] ], model: 'claude-opus-5', tools: [ [ 'name' => 'search_flights', 'strict' => true, 'input_schema' => [ 'type' => 'object', 'properties' => [ 'destination' => ['type' => 'string'], 'departure_date' => ['type' => 'string', 'format' => 'date'], 'passengers' => [ 'type' => 'integer', 'enum' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] ] ], 'required' => ['destination', 'departure_date'], 'additionalProperties' => false ] ] ], ); echo $message; ``` ```ruby Ruby client = Anthropic::Client.new message = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: "Search for flights to Tokyo departing June 1, 2026" } ], tools: [ { name: "search_flights", strict: true, input_schema: { type: "object", properties: { destination: { type: "string" }, departure_date: { type: "string", format: "date" }, passengers: { type: "integer", enum: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] } }, required: ["destination", "departure_date"], additionalProperties: false } } ] ) puts message ``` Build reliable multistep agents with guaranteed tool parameters: ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "messages": [ {"role": "user", "content": "Help me plan a trip from New York to Paris for 2 people, departing June 1, 2026"} ], "tools": [ { "name": "search_flights", "strict": true, "input_schema": { "type": "object", "properties": { "origin": {"type": "string"}, "destination": {"type": "string"}, "departure_date": {"type": "string", "format": "date"}, "travelers": {"type": "integer", "enum": [1, 2, 3, 4, 5, 6]} }, "required": ["origin", "destination", "departure_date"], "additionalProperties": false } }, { "name": "search_hotels", "strict": true, "input_schema": { "type": "object", "properties": { "city": {"type": "string"}, "check_in": {"type": "string", "format": "date"}, "guests": {"type": "integer", "enum": [1, 2, 3, 4]} }, "required": ["city", "check_in"], "additionalProperties": false } } ] }' ``` ```bash CLI ant messages create <<'YAML' model: claude-opus-5 max_tokens: 1024 messages: - role: user content: >- Help me plan a trip from New York to Paris for 2 people, departing June 1, 2026 tools: - name: search_flights strict: true input_schema: type: object properties: origin: {type: string} destination: {type: string} departure_date: {type: string, format: date} travelers: {type: integer, enum: [1, 2, 3, 4, 5, 6]} required: [origin, destination, departure_date] additionalProperties: false - name: search_hotels strict: true input_schema: type: object properties: city: {type: string} check_in: {type: string, format: date} guests: {type: integer, enum: [1, 2, 3, 4]} required: [city, check_in] additionalProperties: false YAML ``` ```python Python client = Anthropic() response = client.messages.create( model="claude-opus-5", max_tokens=1024, messages=[ { "role": "user", "content": "Help me plan a trip from New York to Paris for 2 people, departing June 1, 2026", } ], tools=[ { "name": "search_flights", "strict": True, "input_schema": { "type": "object", "properties": { "origin": {"type": "string"}, "destination": {"type": "string"}, "departure_date": {"type": "string", "format": "date"}, "travelers": {"type": "integer", "enum": [1, 2, 3, 4, 5, 6]}, }, "required": ["origin", "destination", "departure_date"], "additionalProperties": False, }, }, { "name": "search_hotels", "strict": True, "input_schema": { "type": "object", "properties": { "city": {"type": "string"}, "check_in": {"type": "string", "format": "date"}, "guests": {"type": "integer", "enum": [1, 2, 3, 4]}, }, "required": ["city", "check_in"], "additionalProperties": False, }, }, ], ) print(response) ``` ```typescript TypeScript const client = new Anthropic(); const tools: Anthropic.Tool[] = [ { name: "search_flights", strict: true, input_schema: { type: "object", properties: { origin: { type: "string" }, destination: { type: "string" }, departure_date: { type: "string", format: "date" }, travelers: { type: "integer", enum: [1, 2, 3, 4, 5, 6] } }, required: ["origin", "destination", "departure_date"], additionalProperties: false } }, { name: "search_hotels", strict: true, input_schema: { type: "object", properties: { city: { type: "string" }, check_in: { type: "string", format: "date" }, guests: { type: "integer", enum: [1, 2, 3, 4] } }, required: ["city", "check_in"], additionalProperties: false } } ]; const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: "Help me plan a trip from New York to Paris for 2 people, departing June 1, 2026" } ], tools: tools }); console.log(response); ``` ```csharp C# using System.Text.Json; using Anthropic; using Anthropic.Models.Messages; AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [new() { Role = Role.User, Content = "Help me plan a trip from New York to Paris for 2 people, departing June 1, 2026" }], Tools = [ new ToolUnion(new Tool() { Name = "search_flights", Strict = true, InputSchema = new InputSchema(new Dictionary { ["properties"] = JsonSerializer.SerializeToElement(new Dictionary { ["origin"] = new { type = "string" }, ["destination"] = new { type = "string" }, ["departure_date"] = new { type = "string", format = "date" }, ["travelers"] = new { type = "integer", @enum = new[] { 1, 2, 3, 4, 5, 6 } }, }), ["required"] = JsonSerializer.SerializeToElement(new[] { "origin", "destination", "departure_date" }), ["additionalProperties"] = JsonSerializer.SerializeToElement(false), }), }), new ToolUnion(new Tool() { Name = "search_hotels", Strict = true, InputSchema = new InputSchema(new Dictionary { ["properties"] = JsonSerializer.SerializeToElement(new Dictionary { ["city"] = new { type = "string" }, ["check_in"] = new { type = "string", format = "date" }, ["guests"] = new { type = "integer", @enum = new[] { 1, 2, 3, 4 } }, }), ["required"] = JsonSerializer.SerializeToElement(new[] { "city", "check_in" }), ["additionalProperties"] = JsonSerializer.SerializeToElement(false), }), }), ] }; var message = await client.Messages.Create(parameters); Console.WriteLine(message); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Help me plan a trip from New York to Paris for 2 people, departing June 1, 2026")), }, Tools: []anthropic.ToolUnionParam{ {OfTool: &anthropic.ToolParam{ Name: "search_flights", Strict: anthropic.Bool(true), InputSchema: anthropic.ToolInputSchemaParam{ Properties: map[string]any{ "origin": map[string]any{"type": "string"}, "destination": map[string]any{"type": "string"}, "departure_date": map[string]any{"type": "string", "format": "date"}, "travelers": map[string]any{"type": "integer", "enum": []int{1, 2, 3, 4, 5, 6}}, }, Required: []string{"origin", "destination", "departure_date"}, ExtraFields: map[string]any{ "additionalProperties": false, }, }}}, {OfTool: &anthropic.ToolParam{ Name: "search_hotels", Strict: anthropic.Bool(true), InputSchema: anthropic.ToolInputSchemaParam{ Properties: map[string]any{ "city": map[string]any{"type": "string"}, "check_in": map[string]any{"type": "string", "format": "date"}, "guests": map[string]any{"type": "integer", "enum": []int{1, 2, 3, 4}}, }, Required: []string{"city", "check_in"}, ExtraFields: map[string]any{ "additionalProperties": false, }, }}}, }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); InputSchema flightsSchema = InputSchema.builder() .properties( JsonValue.from( Map.of( "origin", Map.of("type", "string"), "destination", Map.of("type", "string"), "departure_date", Map.of("type", "string", "format", "date"), "travelers", Map.of("type", "integer", "enum", List.of(1, 2, 3, 4, 5, 6)) ) ) ) .putAdditionalProperty("required", JsonValue.from(List.of("origin", "destination", "departure_date"))) .putAdditionalProperty("additionalProperties", JsonValue.from(false)) .build(); InputSchema hotelsSchema = InputSchema.builder() .properties( JsonValue.from( Map.of( "city", Map.of("type", "string"), "check_in", Map.of("type", "string", "format", "date"), "guests", Map.of("type", "integer", "enum", List.of(1, 2, 3, 4)) ) ) ) .putAdditionalProperty("required", JsonValue.from(List.of("city", "check_in"))) .putAdditionalProperty("additionalProperties", JsonValue.from(false)) .build(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addUserMessage("Help me plan a trip from New York to Paris for 2 people, departing June 1, 2026") .addTool( Tool.builder() .name("search_flights") .strict(true) .inputSchema(flightsSchema) .build() ) .addTool( Tool.builder() .name("search_hotels") .strict(true) .inputSchema(hotelsSchema) .build() ) .build(); Message response = client.messages().create(params); IO.println(response); ``` ```php PHP $client = new Client(); $message = $client->messages->create( maxTokens: 1024, messages: [ ['role' => 'user', 'content' => 'Help me plan a trip from New York to Paris for 2 people, departing June 1, 2026'] ], model: 'claude-opus-5', tools: [ [ 'name' => 'search_flights', 'strict' => true, 'input_schema' => [ 'type' => 'object', 'properties' => [ 'origin' => ['type' => 'string'], 'destination' => ['type' => 'string'], 'departure_date' => ['type' => 'string', 'format' => 'date'], 'travelers' => ['type' => 'integer', 'enum' => [1, 2, 3, 4, 5, 6]] ], 'required' => ['origin', 'destination', 'departure_date'], 'additionalProperties' => false ] ], [ 'name' => 'search_hotels', 'strict' => true, 'input_schema' => [ 'type' => 'object', 'properties' => [ 'city' => ['type' => 'string'], 'check_in' => ['type' => 'string', 'format' => 'date'], 'guests' => ['type' => 'integer', 'enum' => [1, 2, 3, 4]] ], 'required' => ['city', 'check_in'], 'additionalProperties' => false ] ] ], ); echo $message; ``` ```ruby Ruby client = Anthropic::Client.new message = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: "Help me plan a trip from New York to Paris for 2 people, departing June 1, 2026" } ], tools: [ { name: "search_flights", strict: true, input_schema: { type: "object", properties: { origin: { type: "string" }, destination: { type: "string" }, departure_date: { type: "string", format: "date" }, travelers: { type: "integer", enum: [1, 2, 3, 4, 5, 6] } }, required: ["origin", "destination", "departure_date"], additionalProperties: false } }, { name: "search_hotels", strict: true, input_schema: { type: "object", properties: { city: { type: "string" }, check_in: { type: "string", format: "date" }, guests: { type: "integer", enum: [1, 2, 3, 4] } }, required: ["city", "check_in"], additionalProperties: false } } ] ) puts message ``` ## Data retention Strict tool use compiles tool `input_schema` definitions into grammars using the same pipeline as [structured outputs](https://platform.claude.com/docs/en/build-with-claude/structured-outputs). Tool schemas are temporarily cached for up to 24 hours since last use. Prompts and responses are not retained beyond the API response. Strict tool use is HIPAA eligible, but **protected health information (PHI) must not be included in tool schema definitions**. The API caches compiled schemas separately from message content, and these cached schemas do not receive the same PHI protections as prompts and responses. Do not include PHI in `input_schema` property names, `enum` values, `const` values, or `pattern` regular expressions. PHI should only appear in message content (prompts and responses), where it is protected under HIPAA safeguards. For ZDR and HIPAA eligibility across all features, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention). ## Next steps Fetch and read content from specific URLs to bring live web content into Claude's context. Cache tool definitions across turns to reduce cost and latency. Get validated JSON responses using the same grammar-constrained sampling. Specify tool schemas, write effective descriptions, and control when Claude calls your tools. --- title: Text editor tool url: https://platform.claude.com/docs/en/agents-and-tools/tool-use/text-editor-tool description: Give Claude the Anthropic-defined text editor tool to view, create, and edit files, and handle its view, str_replace, create, and insert commands. --- For how zero data retention (ZDR) applies to this feature, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention). Claude can use an Anthropic-schema text editor tool to view and modify text files, helping you debug, fix, and improve your code or other text documents. This allows Claude to directly interact with your files, providing hands-on assistance rather than just suggesting changes. For model support, see the [Tool reference](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-reference). ## When to use the text editor tool Some examples of when to use the text editor tool are: * **Code debugging:** Have Claude identify and fix bugs in your code, from syntax errors to logic issues. * **Code refactoring:** Let Claude improve your code structure, readability, and performance through targeted edits. * **Documentation generation:** Ask Claude to add docstrings, comments, or README files to your code base. * **Test creation:** Have Claude create unit tests for your code based on its analysis of the implementation. ## Use the text editor tool Provide the text editor tool (named `str_replace_based_edit_tool`) to Claude using the Messages API. You can optionally specify a `max_characters` parameter to control truncation when viewing large files. `max_characters` is only compatible with `text_editor_20250728` and later versions of the text editor tool. ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "tools": [ { "type": "text_editor_20250728", "name": "str_replace_based_edit_tool", "max_characters": 10000 } ], "messages": [ { "role": "user", "content": "There'\''s a syntax error in my primes.py file. Can you help me fix it?" } ] }' ``` ```bash CLI ant messages create \ --model claude-opus-5 \ --max-tokens 1024 \ --tool '{type: text_editor_20250728, name: str_replace_based_edit_tool, max_characters: 10000}' \ --message '{role: user, content: There is a syntax error in my primes.py file. Can you help me fix it?}' ``` ```python Python client = anthropic.Anthropic() response = client.messages.create( model="claude-opus-5", max_tokens=1024, tools=[ { "type": "text_editor_20250728", "name": "str_replace_based_edit_tool", "max_characters": 10000, } ], messages=[ { "role": "user", "content": "There's a syntax error in my primes.py file. Can you help me fix it?", } ], ) print(response) ``` ```typescript TypeScript const anthropic = new Anthropic(); const response = await anthropic.messages.create({ model: "claude-opus-5", max_tokens: 1024, tools: [ { type: "text_editor_20250728", name: "str_replace_based_edit_tool", max_characters: 10000 } ], messages: [ { role: "user", content: "There's a syntax error in my primes.py file. Can you help me fix it?" } ] }); console.log(response); ``` ```csharp C# var client = new AnthropicClient(); var response = await client.Messages.Create( new() { Model = Model.ClaudeOpus5, MaxTokens = 1024, Tools = [new ToolTextEditor20250728 { MaxCharacters = 10000 }], Messages = [ new() { Role = Role.User, Content = "There's a syntax error in my primes.py file. Can you help me fix it?", }, ], } ); Console.WriteLine(response); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Tools: []anthropic.ToolUnionParam{ {OfTextEditor20250728: &anthropic.ToolTextEditor20250728Param{ MaxCharacters: anthropic.Int(10000), }}, }, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("There's a syntax error in my primes.py file. Can you help me fix it?")), }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java import com.anthropic.models.messages.ToolTextEditor20250728; // ... void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); ToolTextEditor20250728 editorTool = ToolTextEditor20250728.builder() .maxCharacters(10000L) .build(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .addTool(editorTool) .addUserMessage("There's a syntax error in my primes.py file. Can you help me fix it?") .build(); Message message = client.messages().create(params); IO.println(message); } ``` ```php PHP $client = new Client(); $response = $client->messages->create( model: 'claude-opus-5', maxTokens: 1024, tools: [ToolTextEditor20250728::with(maxCharacters: 10000)], messages: [ [ 'role' => 'user', 'content' => "There's a syntax error in my primes.py file. Can you help me fix it?", ], ], ); echo $response; ``` ```ruby Ruby client = Anthropic::Client.new response = client.messages.create( model: "claude-opus-5", max_tokens: 1024, tools: [ { type: "text_editor_20250728", name: "str_replace_based_edit_tool", max_characters: 10000 } ], messages: [ { role: "user", content: "There's a syntax error in my primes.py file. Can you help me fix it?" } ] ) puts response ``` Use the text editor tool in the following way: * Include the text editor tool in your API request * Provide a user prompt that may require examining or modifying files, such as "Can you fix the syntax error in my code?" * Claude assesses what it needs to look at and uses the `view` command to examine file contents or list directory contents * The API response will contain a `tool_use` content block with the `view` command * Extract the file or directory path from Claude's tool use request * Read the file's contents or list the directory contents * If a `max_characters` parameter was specified in the tool configuration, truncate the file contents to that length * Return the results to Claude by continuing the conversation with a new `user` message containing a `tool_result` content block * After examining the file or directory, Claude may use a command such as `str_replace` to make changes or `insert` to add text at a specific line number. * If Claude uses the `str_replace` command, Claude constructs a properly formatted tool use request with the old text and new text to replace it with * Extract the file path, old text, and new text from Claude's tool use request * Perform the text replacement in the file * Return the results to Claude * After examining and possibly editing the files, Claude provides a complete explanation of what it found and what changes it made ### Text editor tool commands The text editor tool supports several commands for viewing and modifying files: #### view The `view` command allows Claude to examine the contents of a file or list the contents of a directory. It can read the entire file or a specific range of lines. Parameters: * `command`: Must be "view" * `path`: The path to the file or directory to view * `view_range` (optional): An array of two integers specifying the start and end line numbers to view. Line numbers are 1-indexed, and -1 for the end line means read to the end of the file. This parameter only applies when viewing files, not directories. Example for viewing a file: ```json { "type": "tool_use", "id": "toolu_01A09q90qw90lq917835lq9", "name": "str_replace_based_edit_tool", "input": { "command": "view", "path": "primes.py" } } ``` Example for viewing a directory: ```json { "type": "tool_use", "id": "toolu_02B19r91rw91mr917835mr9", "name": "str_replace_based_edit_tool", "input": { "command": "view", "path": "src/" } } ``` #### str\_replace The `str_replace` command allows Claude to replace a specific string in a file with a new string. This is used for making precise edits. Parameters: * `command`: Must be "str\_replace" * `path`: The path to the file to modify * `old_str`: The text to replace (must match exactly, including whitespace and indentation) * `new_str`: The new text to insert in place of the old text ```json { "type": "tool_use", "id": "toolu_01A09q90qw90lq917835lq9", "name": "str_replace_based_edit_tool", "input": { "command": "str_replace", "path": "primes.py", "old_str": "for num in range(2, limit + 1)", "new_str": "for num in range(2, limit + 1):" } } ``` #### create The `create` command allows Claude to create a new file with specified content. Parameters: * `command`: Must be "create" * `path`: The path where the new file should be created * `file_text`: The content to write to the new file ```json { "type": "tool_use", "id": "toolu_01A09q90qw90lq917835lq9", "name": "str_replace_based_edit_tool", "input": { "command": "create", "path": "test_primes.py", "file_text": "import unittest\nimport primes\n\nclass TestPrimes(unittest.TestCase):\n def test_is_prime(self):\n self.assertTrue(primes.is_prime(2))\n self.assertTrue(primes.is_prime(3))\n self.assertFalse(primes.is_prime(4))\n\nif __name__ == '__main__':\n unittest.main()" } } ``` #### insert The `insert` command allows Claude to insert text at a specific location in a file. Parameters: * `command`: Must be "insert" * `path`: The path to the file to modify * `insert_line`: The line number after which to insert the text (0 for beginning of file) * `insert_text`: The text to insert ```json { "type": "tool_use", "id": "toolu_01A09q90qw90lq917835lq9", "name": "str_replace_based_edit_tool", "input": { "command": "insert", "path": "primes.py", "insert_line": 0, "insert_text": "\"\"\"Module for working with prime numbers.\n\nThis module provides functions to check if a number is prime\nand to generate a list of prime numbers up to a given limit.\n\"\"\"\n" } } ``` ### Example: Fixing a syntax error with the text editor tool This example demonstrates how Claude uses the text editor tool to fix a syntax error in a Python file. First, your application provides Claude with the text editor tool and a prompt to fix a syntax error: ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "tools": [ { "type": "text_editor_20250728", "name": "str_replace_based_edit_tool" } ], "messages": [ { "role": "user", "content": "There'\''s a syntax error in my primes.py file. Can you help me fix it?" } ] }' ``` ```bash CLI ant messages create \ --model claude-opus-5 \ --max-tokens 1024 \ --tool '{type: text_editor_20250728, name: str_replace_based_edit_tool}' \ --message '{role: user, content: There is a syntax error in my primes.py file. Can you help me fix it?}' ``` ```python Python client = anthropic.Anthropic() response = client.messages.create( model="claude-opus-5", max_tokens=1024, tools=[{"type": "text_editor_20250728", "name": "str_replace_based_edit_tool"}], messages=[ { "role": "user", "content": "There's a syntax error in my primes.py file. Can you help me fix it?", } ], ) print(response) ``` ```typescript TypeScript const anthropic = new Anthropic(); const response = await anthropic.messages.create({ model: "claude-opus-5", max_tokens: 1024, tools: [ { type: "text_editor_20250728", name: "str_replace_based_edit_tool" } ], messages: [ { role: "user", content: "There's a syntax error in my primes.py file. Can you help me fix it?" } ] }); console.log(response); ``` ```csharp C# var client = new AnthropicClient(); var response = await client.Messages.Create( new() { Model = Model.ClaudeOpus5, MaxTokens = 1024, Tools = [new ToolTextEditor20250728()], Messages = [ new() { Role = Role.User, Content = "There's a syntax error in my primes.py file. Can you help me fix it?", }, ], } ); Console.WriteLine(response); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Tools: []anthropic.ToolUnionParam{ {OfTextEditor20250728: &anthropic.ToolTextEditor20250728Param{}}, }, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("There's a syntax error in my primes.py file. Can you help me fix it?")), }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java import com.anthropic.models.messages.ToolTextEditor20250728; // ... void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); ToolTextEditor20250728 editorTool = ToolTextEditor20250728.builder().build(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .addTool(editorTool) .addUserMessage("There's a syntax error in my primes.py file. Can you help me fix it?") .build(); Message message = client.messages().create(params); IO.println(message); } ``` ```php PHP $client = new Client(); $response = $client->messages->create( model: 'claude-opus-5', maxTokens: 1024, tools: [new ToolTextEditor20250728()], messages: [ [ 'role' => 'user', 'content' => "There's a syntax error in my primes.py file. Can you help me fix it?", ], ], ); echo $response; ``` ```ruby Ruby client = Anthropic::Client.new response = client.messages.create( model: "claude-opus-5", max_tokens: 1024, tools: [{type: "text_editor_20250728", name: "str_replace_based_edit_tool"}], messages: [ { role: "user", content: "There's a syntax error in my primes.py file. Can you help me fix it?" } ] ) puts response ``` Claude uses the text editor tool first to view the file: ```json Output { "id": "msg_01XAbCDeFgHiJkLmNoPQrStU", "model": "claude-opus-5", "stop_reason": "tool_use", "role": "assistant", "content": [ { "type": "text", "text": "I'll help you fix the syntax error in your primes.py file. First, let me take a look at the file to identify the issue." }, { "type": "tool_use", "id": "toolu_01AbCdEfGhIjKlMnOpQrStU", "name": "str_replace_based_edit_tool", "input": { "command": "view", "path": "primes.py" } } ] } ``` Your application should then read the file and return its contents to Claude: ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "tools": [ { "type": "text_editor_20250728", "name": "str_replace_based_edit_tool" } ], "messages": [ { "role": "user", "content": "There'\''s a syntax error in my primes.py file. Can you help me fix it?" }, { "role": "assistant", "content": [ { "type": "text", "text": "I'\''ll help you fix the syntax error in your primes.py file. First, let me take a look at the file to identify the issue." }, { "type": "tool_use", "id": "toolu_01AbCdEfGhIjKlMnOpQrStU", "name": "str_replace_based_edit_tool", "input": { "command": "view", "path": "primes.py" } } ] }, { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_01AbCdEfGhIjKlMnOpQrStU", "content": "1: def is_prime(n):\n2: \"\"\"Check if a number is prime.\"\"\"\n3: if n <= 1:\n4: return False\n5: if n <= 3:\n6: return True\n7: if n % 2 == 0 or n % 3 == 0:\n8: return False\n9: i = 5\n10: while i * i <= n:\n11: if n % i == 0 or n % (i + 2) == 0:\n12: return False\n13: i += 6\n14: return True\n15: \n16: def get_primes(limit):\n17: \"\"\"Generate a list of prime numbers up to the given limit.\"\"\"\n18: primes = []\n19: for num in range(2, limit + 1)\n20: if is_prime(num):\n21: primes.append(num)\n22: return primes\n23: \n24: def main():\n25: \"\"\"Main function to demonstrate prime number generation.\"\"\"\n26: limit = 100\n27: prime_list = get_primes(limit)\n28: print(f\"Prime numbers up to {limit}:\")\n29: print(prime_list)\n30: print(f\"Found {len(prime_list)} prime numbers.\")\n31: \n32: if __name__ == \"__main__\":\n33: main()" } ] } ] }' ``` ```bash CLI ant messages create <<'YAML' model: claude-opus-5 max_tokens: 1024 tools: - type: text_editor_20250728 name: str_replace_based_edit_tool messages: - role: user content: There's a syntax error in my primes.py file. Can you help me fix it? - role: assistant content: - type: text text: >- I'll help you fix the syntax error in your primes.py file. First, let me take a look at the file to identify the issue. - type: tool_use id: toolu_01AbCdEfGhIjKlMnOpQrStU name: str_replace_based_edit_tool input: command: view path: primes.py - role: user content: - type: tool_result tool_use_id: toolu_01AbCdEfGhIjKlMnOpQrStU content: |- 1: def is_prime(n): 2: """Check if a number is prime.""" 3: if n <= 1: 4: return False 5: if n <= 3: 6: return True 7: if n % 2 == 0 or n % 3 == 0: 8: return False 9: i = 5 10: while i * i <= n: 11: if n % i == 0 or n % (i + 2) == 0: 12: return False 13: i += 6 14: return True 15: 16: def get_primes(limit): 17: """Generate a list of prime numbers up to the given limit.""" 18: primes = [] 19: for num in range(2, limit + 1) 20: if is_prime(num): 21: primes.append(num) 22: return primes 23: 24: def main(): 25: """Main function to demonstrate prime number generation.""" 26: limit = 100 27: prime_list = get_primes(limit) 28: print(f"Prime numbers up to {limit}:") 29: print(prime_list) 30: print(f"Found {len(prime_list)} prime numbers.") 31: 32: if __name__ == "__main__": 33: main() YAML ``` ```python Python response = client.messages.create( model="claude-opus-5", max_tokens=1024, tools=[{"type": "text_editor_20250728", "name": "str_replace_based_edit_tool"}], messages=[ { "role": "user", "content": "There's a syntax error in my primes.py file. Can you help me fix it?", }, { "role": "assistant", "content": [ { "type": "text", "text": "I'll help you fix the syntax error in your primes.py file. First, let me take a look at the file to identify the issue.", }, { "type": "tool_use", "id": "toolu_01AbCdEfGhIjKlMnOpQrStU", "name": "str_replace_based_edit_tool", "input": {"command": "view", "path": "primes.py"}, }, ], }, { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_01AbCdEfGhIjKlMnOpQrStU", "content": '1: def is_prime(n):\n2: """Check if a number is prime."""\n3: if n <= 1:\n4: return False\n5: if n <= 3:\n6: return True\n7: if n % 2 == 0 or n % 3 == 0:\n8: return False\n9: i = 5\n10: while i * i <= n:\n11: if n % i == 0 or n % (i + 2) == 0:\n12: return False\n13: i += 6\n14: return True\n15: \n16: def get_primes(limit):\n17: """Generate a list of prime numbers up to the given limit."""\n18: primes = []\n19: for num in range(2, limit + 1)\n20: if is_prime(num):\n21: primes.append(num)\n22: return primes\n23: \n24: def main():\n25: """Main function to demonstrate prime number generation."""\n26: limit = 100\n27: prime_list = get_primes(limit)\n28: print(f"Prime numbers up to {limit}:")\n29: print(prime_list)\n30: print(f"Found {len(prime_list)} prime numbers.")\n31: \n32: if __name__ == "__main__":\n33: main()', } ], }, ], ) print(response) ``` ```typescript TypeScript const anthropic = new Anthropic(); const response = await anthropic.messages.create({ model: "claude-opus-5", max_tokens: 1024, tools: [ { type: "text_editor_20250728", name: "str_replace_based_edit_tool" } ], messages: [ { role: "user", content: "There's a syntax error in my primes.py file. Can you help me fix it?" }, { role: "assistant", content: [ { type: "text", text: "I'll help you fix the syntax error in your primes.py file. First, let me take a look at the file to identify the issue." }, { type: "tool_use", id: "toolu_01AbCdEfGhIjKlMnOpQrStU", name: "str_replace_based_edit_tool", input: { command: "view", path: "primes.py" } } ] }, { role: "user", content: [ { type: "tool_result", tool_use_id: "toolu_01AbCdEfGhIjKlMnOpQrStU", content: '1: def is_prime(n):\n2: """Check if a number is prime."""\n3: if n <= 1:\n4: return False\n5: if n <= 3:\n6: return True\n7: if n % 2 == 0 or n % 3 == 0:\n8: return False\n9: i = 5\n10: while i * i <= n:\n11: if n % i == 0 or n % (i + 2) == 0:\n12: return False\n13: i += 6\n14: return True\n15: \n16: def get_primes(limit):\n17: """Generate a list of prime numbers up to the given limit."""\n18: primes = []\n19: for num in range(2, limit + 1)\n20: if is_prime(num):\n21: primes.append(num)\n22: return primes\n23: \n24: def main():\n25: """Main function to demonstrate prime number generation."""\n26: limit = 100\n27: prime_list = get_primes(limit)\n28: print(f"Prime numbers up to {limit}:")\n29: print(prime_list)\n30: print(f"Found {len(prime_list)} prime numbers.")\n31: \n32: if __name__ == "__main__":\n33: main()' } ] } ] }); console.log(response); ``` ```csharp C# var client = new AnthropicClient(); var response = await client.Messages.Create( new() { Model = Model.ClaudeOpus5, MaxTokens = 1024, Tools = [new ToolTextEditor20250728()], Messages = [ new() { Role = Role.User, Content = "There's a syntax error in my primes.py file. Can you help me fix it?", }, new() { Role = Role.Assistant, Content = new MessageParamContent(new List { new ContentBlockParam(new TextBlockParam() { Text = "I'll help you fix the syntax error in your primes.py file. First, let me take a look at the file to identify the issue.", }), new ContentBlockParam(new ToolUseBlockParam() { ID = "toolu_01AbCdEfGhIjKlMnOpQrStU", Name = "str_replace_based_edit_tool", Input = new Dictionary { ["command"] = JsonSerializer.SerializeToElement("view"), ["path"] = JsonSerializer.SerializeToElement("primes.py"), }, }), }), }, new() { Role = Role.User, Content = new MessageParamContent(new List { new ContentBlockParam(new ToolResultBlockParam() { ToolUseID = "toolu_01AbCdEfGhIjKlMnOpQrStU", Content = "1: def is_prime(n):\n2: \"\"\"Check if a number is prime.\"\"\"\n3: if n <= 1:\n4: return False\n5: if n <= 3:\n6: return True\n7: if n % 2 == 0 or n % 3 == 0:\n8: return False\n9: i = 5\n10: while i * i <= n:\n11: if n % i == 0 or n % (i + 2) == 0:\n12: return False\n13: i += 6\n14: return True\n15: \n16: def get_primes(limit):\n17: \"\"\"Generate a list of prime numbers up to the given limit.\"\"\"\n18: primes = []\n19: for num in range(2, limit + 1)\n20: if is_prime(num):\n21: primes.append(num)\n22: return primes\n23: \n24: def main():\n25: \"\"\"Main function to demonstrate prime number generation.\"\"\"\n26: limit = 100\n27: prime_list = get_primes(limit)\n28: print(f\"Prime numbers up to {limit}:\")\n29: print(prime_list)\n30: print(f\"Found {len(prime_list)} prime numbers.\")\n31: \n32: if __name__ == \"__main__\":\n33: main()", }), }), }, ], } ); Console.WriteLine(response); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Tools: []anthropic.ToolUnionParam{ {OfTextEditor20250728: &anthropic.ToolTextEditor20250728Param{}}, }, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("There's a syntax error in my primes.py file. Can you help me fix it?")), anthropic.NewAssistantMessage( anthropic.NewTextBlock("I'll help you fix the syntax error in your primes.py file. First, let me take a look at the file to identify the issue."), anthropic.NewToolUseBlock( "toolu_01AbCdEfGhIjKlMnOpQrStU", map[string]any{"command": "view", "path": "primes.py"}, "str_replace_based_edit_tool", ), ), anthropic.NewUserMessage( anthropic.NewToolResultBlock( "toolu_01AbCdEfGhIjKlMnOpQrStU", "1: def is_prime(n):\n2: \"\"\"Check if a number is prime.\"\"\"\n3: if n <= 1:\n4: return False\n5: if n <= 3:\n6: return True\n7: if n % 2 == 0 or n % 3 == 0:\n8: return False\n9: i = 5\n10: while i * i <= n:\n11: if n % i == 0 or n % (i + 2) == 0:\n12: return False\n13: i += 6\n14: return True\n15: \n16: def get_primes(limit):\n17: \"\"\"Generate a list of prime numbers up to the given limit.\"\"\"\n18: primes = []\n19: for num in range(2, limit + 1)\n20: if is_prime(num):\n21: primes.append(num)\n22: return primes\n23: \n24: def main():\n25: \"\"\"Main function to demonstrate prime number generation.\"\"\"\n26: limit = 100\n27: prime_list = get_primes(limit)\n28: print(f\"Prime numbers up to {limit}:\")\n29: print(prime_list)\n30: print(f\"Found {len(prime_list)} prime numbers.\")\n31: \n32: if __name__ == \"__main__\":\n33: main()", false, ), ), }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .addTool(ToolTextEditor20250728.builder().build()) .addUserMessage("There's a syntax error in my primes.py file. Can you help me fix it?") .addAssistantMessageOfBlockParams( List.of( ContentBlockParam.ofText( TextBlockParam.builder() .text("I'll help you fix the syntax error in your primes.py file. First, let me take a look at the file to identify the issue.") .build() ), ContentBlockParam.ofToolUse( ToolUseBlockParam.builder() .id("toolu_01AbCdEfGhIjKlMnOpQrStU") .name("str_replace_based_edit_tool") .input( ToolUseBlockParam.Input.builder() .putAdditionalProperty("command", JsonValue.from("view")) .putAdditionalProperty("path", JsonValue.from("primes.py")) .build() ) .build() ) ) ) .addUserMessageOfBlockParams( List.of( ContentBlockParam.ofToolResult( ToolResultBlockParam.builder() .toolUseId("toolu_01AbCdEfGhIjKlMnOpQrStU") .content("1: def is_prime(n):\n2: \"\"\"Check if a number is prime.\"\"\"\n3: if n <= 1:\n4: return False\n5: if n <= 3:\n6: return True\n7: if n % 2 == 0 or n % 3 == 0:\n8: return False\n9: i = 5\n10: while i * i <= n:\n11: if n % i == 0 or n % (i + 2) == 0:\n12: return False\n13: i += 6\n14: return True\n15: \n16: def get_primes(limit):\n17: \"\"\"Generate a list of prime numbers up to the given limit.\"\"\"\n18: primes = []\n19: for num in range(2, limit + 1)\n20: if is_prime(num):\n21: primes.append(num)\n22: return primes\n23: \n24: def main():\n25: \"\"\"Main function to demonstrate prime number generation.\"\"\"\n26: limit = 100\n27: prime_list = get_primes(limit)\n28: print(f\"Prime numbers up to {limit}:\")\n29: print(prime_list)\n30: print(f\"Found {len(prime_list)} prime numbers.\")\n31: \n32: if __name__ == \"__main__\":\n33: main()") .build() ) ) ) .build(); Message message = client.messages().create(params); System.out.println(message); ``` ```php PHP $client = new Client(); $response = $client->messages->create( model: 'claude-opus-5', maxTokens: 1024, tools: [new ToolTextEditor20250728()], messages: [ [ 'role' => 'user', 'content' => "There's a syntax error in my primes.py file. Can you help me fix it?", ], [ 'role' => 'assistant', 'content' => [ [ 'type' => 'text', 'text' => "I'll help you fix the syntax error in your primes.py file. First, let me take a look at the file to identify the issue.", ], [ 'type' => 'tool_use', 'id' => 'toolu_01AbCdEfGhIjKlMnOpQrStU', 'name' => 'str_replace_based_edit_tool', 'input' => ['command' => 'view', 'path' => 'primes.py'], ], ], ], [ 'role' => 'user', 'content' => [ [ 'type' => 'tool_result', 'tool_use_id' => 'toolu_01AbCdEfGhIjKlMnOpQrStU', 'content' => "1: def is_prime(n):\n2: \"\"\"Check if a number is prime.\"\"\"\n3: if n <= 1:\n4: return False\n5: if n <= 3:\n6: return True\n7: if n % 2 == 0 or n % 3 == 0:\n8: return False\n9: i = 5\n10: while i * i <= n:\n11: if n % i == 0 or n % (i + 2) == 0:\n12: return False\n13: i += 6\n14: return True\n15: \n16: def get_primes(limit):\n17: \"\"\"Generate a list of prime numbers up to the given limit.\"\"\"\n18: primes = []\n19: for num in range(2, limit + 1)\n20: if is_prime(num):\n21: primes.append(num)\n22: return primes\n23: \n24: def main():\n25: \"\"\"Main function to demonstrate prime number generation.\"\"\"\n26: limit = 100\n27: prime_list = get_primes(limit)\n28: print(f\"Prime numbers up to {limit}:\")\n29: print(prime_list)\n30: print(f\"Found {len(prime_list)} prime numbers.\")\n31: \n32: if __name__ == \"__main__\":\n33: main()", ], ], ], ], ); echo $response; ``` ```ruby Ruby client = Anthropic::Client.new response = client.messages.create( model: "claude-opus-5", max_tokens: 1024, tools: [{type: "text_editor_20250728", name: "str_replace_based_edit_tool"}], messages: [ { role: "user", content: "There's a syntax error in my primes.py file. Can you help me fix it?" }, { role: "assistant", content: [ { type: "text", text: "I'll help you fix the syntax error in your primes.py file. First, let me take a look at the file to identify the issue." }, { type: "tool_use", id: "toolu_01AbCdEfGhIjKlMnOpQrStU", name: "str_replace_based_edit_tool", input: {command: "view", path: "primes.py"} } ] }, { role: "user", content: [ { type: "tool_result", tool_use_id: "toolu_01AbCdEfGhIjKlMnOpQrStU", content: "1: def is_prime(n):\n2: \"\"\"Check if a number is prime.\"\"\"\n3: if n <= 1:\n4: return False\n5: if n <= 3:\n6: return True\n7: if n % 2 == 0 or n % 3 == 0:\n8: return False\n9: i = 5\n10: while i * i <= n:\n11: if n % i == 0 or n % (i + 2) == 0:\n12: return False\n13: i += 6\n14: return True\n15: \n16: def get_primes(limit):\n17: \"\"\"Generate a list of prime numbers up to the given limit.\"\"\"\n18: primes = []\n19: for num in range(2, limit + 1)\n20: if is_prime(num):\n21: primes.append(num)\n22: return primes\n23: \n24: def main():\n25: \"\"\"Main function to demonstrate prime number generation.\"\"\"\n26: limit = 100\n27: prime_list = get_primes(limit)\n28: print(f\"Prime numbers up to {limit}:\")\n29: print(prime_list)\n30: print(f\"Found {len(prime_list)} prime numbers.\")\n31: \n32: if __name__ == \"__main__\":\n33: main()" } ] } ] ) puts response ``` **Line numbers** In the preceding example, the `view` tool result includes file contents with line numbers prepended to each line (for example, "1: def is\_prime(n):"). Line numbers are not required, but they are essential for successfully using the `view_range` parameter to examine specific sections of files and the `insert_line` parameter to add content at precise locations. Claude identifies the syntax error and uses the `str_replace` command to fix it: ```json Output { "id": "msg_01VwXyZAbCdEfGhIjKlMnO", "model": "claude-opus-5", "stop_reason": "tool_use", "role": "assistant", "content": [ { "type": "text", "text": "I found the syntax error in your primes.py file. In the `get_primes` function, there is a missing colon (:) at the end of the for loop line. Let me fix that for you." }, { "type": "tool_use", "id": "toolu_01PqRsTuVwXyZAbCdEfGh", "name": "str_replace_based_edit_tool", "input": { "command": "str_replace", "path": "primes.py", "old_str": " for num in range(2, limit + 1)", "new_str": " for num in range(2, limit + 1):" } } ] } ``` Your application should then make the edit and return the result: ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "tools": [ { "type": "text_editor_20250728", "name": "str_replace_based_edit_tool" } ], "messages": [ { "role": "assistant", "content": [ { "type": "text", "text": "I found the syntax error in your primes.py file. In the `get_primes` function, there is a missing colon (:) at the end of the for loop line. Let me fix that for you." }, { "type": "tool_use", "id": "toolu_01PqRsTuVwXyZAbCdEfGh", "name": "str_replace_based_edit_tool", "input": { "command": "str_replace", "path": "primes.py", "old_str": " for num in range(2, limit + 1)", "new_str": " for num in range(2, limit + 1):" } } ] }, { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_01PqRsTuVwXyZAbCdEfGh", "content": "Successfully replaced text at exactly one location." } ] } ] }' ``` ```bash CLI ant messages create <<'YAML' model: claude-opus-5 max_tokens: 1024 tools: - type: text_editor_20250728 name: str_replace_based_edit_tool messages: # Previous messages... - role: assistant content: - type: text text: >- I found the syntax error in your primes.py file. In the `get_primes` function, there is a missing colon (:) at the end of the for loop line. Let me fix that for you. - type: tool_use id: toolu_01PqRsTuVwXyZAbCdEfGh name: str_replace_based_edit_tool input: command: str_replace path: primes.py old_str: " for num in range(2, limit + 1)" new_str: " for num in range(2, limit + 1):" - role: user content: - type: tool_result tool_use_id: toolu_01PqRsTuVwXyZAbCdEfGh content: Successfully replaced text at exactly one location. YAML ``` ```python Python response = client.messages.create( model="claude-opus-5", max_tokens=1024, tools=[{"type": "text_editor_20250728", "name": "str_replace_based_edit_tool"}], messages=[ # Previous messages... { "role": "assistant", "content": [ { "type": "text", "text": "I found the syntax error in your primes.py file. In the `get_primes` function, there is a missing colon (:) at the end of the for loop line. Let me fix that for you.", }, { "type": "tool_use", "id": "toolu_01PqRsTuVwXyZAbCdEfGh", "name": "str_replace_based_edit_tool", "input": { "command": "str_replace", "path": "primes.py", "old_str": " for num in range(2, limit + 1)", "new_str": " for num in range(2, limit + 1):", }, }, ], }, { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_01PqRsTuVwXyZAbCdEfGh", "content": "Successfully replaced text at exactly one location.", } ], }, ], ) print(response) ``` ```typescript TypeScript const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, tools: [ { type: "text_editor_20250728", name: "str_replace_based_edit_tool" } ], messages: [ // Previous messages... { role: "assistant", content: [ { type: "text", text: "I found the syntax error in your primes.py file. In the `get_primes` function, there is a missing colon (:) at the end of the for loop line. Let me fix that for you." }, { type: "tool_use", id: "toolu_01PqRsTuVwXyZAbCdEfGh", name: "str_replace_based_edit_tool", input: { command: "str_replace", path: "primes.py", old_str: " for num in range(2, limit + 1)", new_str: " for num in range(2, limit + 1):" } } ] }, { role: "user", content: [ { type: "tool_result", tool_use_id: "toolu_01PqRsTuVwXyZAbCdEfGh", content: "Successfully replaced text at exactly one location." } ] } ] }); console.log(response); ``` ```csharp C# var client = new AnthropicClient(); var response = await client.Messages.Create( new() { Model = Model.ClaudeOpus5, MaxTokens = 1024, Tools = [new ToolTextEditor20250728()], Messages = [ // Previous messages... new() { Role = Role.Assistant, Content = new MessageParamContent(new List { new ContentBlockParam(new TextBlockParam() { Text = "I found the syntax error in your primes.py file. In the `get_primes` function, there is a missing colon (:) at the end of the for loop line. Let me fix that for you.", }), new ContentBlockParam(new ToolUseBlockParam() { ID = "toolu_01PqRsTuVwXyZAbCdEfGh", Name = "str_replace_based_edit_tool", Input = new Dictionary { ["command"] = JsonSerializer.SerializeToElement("str_replace"), ["path"] = JsonSerializer.SerializeToElement("primes.py"), ["old_str"] = JsonSerializer.SerializeToElement(" for num in range(2, limit + 1)"), ["new_str"] = JsonSerializer.SerializeToElement(" for num in range(2, limit + 1):"), }, }), }), }, new() { Role = Role.User, Content = new MessageParamContent(new List { new ContentBlockParam(new ToolResultBlockParam() { ToolUseID = "toolu_01PqRsTuVwXyZAbCdEfGh", Content = "Successfully replaced text at exactly one location.", }), }), }, ], } ); Console.WriteLine(response); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Tools: []anthropic.ToolUnionParam{ {OfTextEditor20250728: &anthropic.ToolTextEditor20250728Param{}}, }, Messages: []anthropic.MessageParam{ // Previous messages... anthropic.NewAssistantMessage( anthropic.NewTextBlock("I found the syntax error in your primes.py file. In the `get_primes` function, there is a missing colon (:) at the end of the for loop line. Let me fix that for you."), anthropic.NewToolUseBlock( "toolu_01PqRsTuVwXyZAbCdEfGh", map[string]any{ "command": "str_replace", "path": "primes.py", "old_str": " for num in range(2, limit + 1)", "new_str": " for num in range(2, limit + 1):", }, "str_replace_based_edit_tool", ), ), anthropic.NewUserMessage( anthropic.NewToolResultBlock( "toolu_01PqRsTuVwXyZAbCdEfGh", "Successfully replaced text at exactly one location.", false, ), ), }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .addTool(ToolTextEditor20250728.builder().build()) // Previous messages would go here .addAssistantMessageOfBlockParams( List.of( ContentBlockParam.ofText( TextBlockParam.builder() .text( "I found the syntax error in your primes.py file. In the `get_primes` function, there is a missing colon (:) at the end of the for loop line. Let me fix that for you." ) .build() ), ContentBlockParam.ofToolUse( ToolUseBlockParam.builder() .id("toolu_01PqRsTuVwXyZAbCdEfGh") .name("str_replace_based_edit_tool") .input( ToolUseBlockParam.Input.builder() .putAdditionalProperty("command", JsonValue.from("str_replace")) .putAdditionalProperty("path", JsonValue.from("primes.py")) .putAdditionalProperty( "old_str", JsonValue.from(" for num in range(2, limit + 1)") ) .putAdditionalProperty( "new_str", JsonValue.from(" for num in range(2, limit + 1):") ) .build() ) .build() ) ) ) .addUserMessageOfBlockParams( List.of( ContentBlockParam.ofToolResult( ToolResultBlockParam.builder() .toolUseId("toolu_01PqRsTuVwXyZAbCdEfGh") .content("Successfully replaced text at exactly one location.") .build() ) ) ) .build(); Message message = client.messages().create(params); System.out.println(message); ``` ```php PHP $client = new Client(); $response = $client->messages->create( model: 'claude-opus-5', maxTokens: 1024, tools: [new ToolTextEditor20250728()], messages: [ // Previous messages... [ 'role' => 'assistant', 'content' => [ [ 'type' => 'text', 'text' => 'I found the syntax error in your primes.py file. In the `get_primes` function, there is a missing colon (:) at the end of the for loop line. Let me fix that for you.', ], [ 'type' => 'tool_use', 'id' => 'toolu_01PqRsTuVwXyZAbCdEfGh', 'name' => 'str_replace_based_edit_tool', 'input' => [ 'command' => 'str_replace', 'path' => 'primes.py', 'old_str' => ' for num in range(2, limit + 1)', 'new_str' => ' for num in range(2, limit + 1):', ], ], ], ], [ 'role' => 'user', 'content' => [ [ 'type' => 'tool_result', 'tool_use_id' => 'toolu_01PqRsTuVwXyZAbCdEfGh', 'content' => 'Successfully replaced text at exactly one location.', ], ], ], ], ); echo $response; ``` ```ruby Ruby client = Anthropic::Client.new response = client.messages.create( model: "claude-opus-5", max_tokens: 1024, tools: [{type: "text_editor_20250728", name: "str_replace_based_edit_tool"}], messages: [ # Previous messages... { role: "assistant", content: [ { type: "text", text: "I found the syntax error in your primes.py file. In the `get_primes` function, there is a missing colon (:) at the end of the for loop line. Let me fix that for you." }, { type: "tool_use", id: "toolu_01PqRsTuVwXyZAbCdEfGh", name: "str_replace_based_edit_tool", input: { command: "str_replace", path: "primes.py", old_str: " for num in range(2, limit + 1)", new_str: " for num in range(2, limit + 1):" } } ] }, { role: "user", content: [ { type: "tool_result", tool_use_id: "toolu_01PqRsTuVwXyZAbCdEfGh", content: "Successfully replaced text at exactly one location." } ] } ] ) puts response ``` Finally, Claude provides a complete explanation of the fix: ````json Output { "id": "msg_01IjKlMnOpQrStUvWxYzAb", "model": "claude-opus-5", "stop_reason": "end_turn", "role": "assistant", "content": [ { "type": "text", "text": "I've fixed the syntax error in your primes.py file. The issue was in the `get_primes` function at line 19. There was a missing colon (:) at the end of the for loop line.\n\nHere's what I changed:\n\nFrom:\n```python\nfor num in range(2, limit + 1)\n```\n\nTo:\n```python\nfor num in range(2, limit + 1):\n```\n\nIn Python, a colon is required at the end of statements that start a new code block, such as for loops, if statements, function definitions, etc. Without the colon, Python raises a syntax error because it doesn't know where the code block begins.\n\nYour code should now run correctly. The function will properly generate a list of prime numbers up to the specified limit." } ] } ```` ## Implement the text editor tool The text editor tool is implemented as a schema-less tool. When using this tool, you don't need to provide an input schema as with other tools; the schema is built into Claude's model and can't be modified. The tool type is `type: "text_editor_20250728"` for Claude 4 and later models. Create helper functions to handle file operations like reading, writing, and modifying files. Consider implementing backup functionality to recover from mistakes. Create a function that processes tool calls from Claude based on the command type: ```python Python def handle_editor_tool(tool_call): input_params = tool_call.input command = input_params.get("command", "") file_path = input_params.get("path", "") if command == "view": # Read and return file contents pass elif command == "str_replace": # Replace text in file pass elif command == "create": # Create new file pass elif command == "insert": # Insert text at location pass ``` ```typescript TypeScript function handleEditorTool(toolCall: { input: { command?: string; path?: string } }): void { const inputParams = toolCall.input; const command = inputParams.command ?? ""; const filePath = inputParams.path ?? ""; if (command === "view") { // Read and return file contents } else if (command === "str_replace") { // Replace text in file } else if (command === "create") { // Create new file } else if (command === "insert") { // Insert text at location } } ``` ```csharp C# static string HandleEditorTool(IReadOnlyDictionary input) { input.TryGetValue("command", out var commandEl); input.TryGetValue("path", out var pathEl); var command = commandEl.ValueKind == JsonValueKind.String ? commandEl.GetString() : null; var filePath = pathEl.ValueKind == JsonValueKind.String ? pathEl.GetString() : null; if (command == "view") { // Read and return file contents } else if (command == "str_replace") { // Replace text in file } else if (command == "create") { // Create new file } else if (command == "insert") { // Insert text at location } return ""; } ``` ```go Go func handleEditorTool(input map[string]any) string { command, _ := input["command"].(string) filePath, _ := input["path"].(string) // ... switch command { case "view": // Read and return file contents case "str_replace": // Replace text in file case "create": // Create new file case "insert": // Insert text at location } return "" } ``` ```java Java static void handleEditorTool(Map input) { var command = (String) input.getOrDefault("command", ""); var filePath = (String) input.getOrDefault("path", ""); if (command.equals("view")) { // Read and return file contents } else if (command.equals("str_replace")) { // Replace text in file } else if (command.equals("create")) { // Create new file } else if (command.equals("insert")) { // Insert text at location } } ``` ```php PHP function handle_editor_tool(array $input): string { $command = $input['command'] ?? ''; $filePath = $input['path'] ?? ''; if ($command === 'view') { // Read and return file contents } elseif ($command === 'str_replace') { // Replace text in file } elseif ($command === 'create') { // Create new file } elseif ($command === 'insert') { // Insert text at location } return ''; } ``` ```ruby Ruby def handle_editor_tool(input) command = input[:command] || "" file_path = input[:path] || "" case command when "view" # Read and return file contents when "str_replace" # Replace text in file when "create" # Create new file when "insert" # Insert text at location end end ``` Add validation and security checks: * Validate file paths to prevent directory traversal * Create backups before making changes * Handle errors gracefully * Implement permissions checks Extract and handle tool calls from Claude's responses: ```python Python # Process tool use in Claude's response for content in response.content: if content.type == "tool_use": # Execute the tool based on command result = handle_editor_tool(content) # Return result to Claude tool_result = { "type": "tool_result", "tool_use_id": content.id, "content": result, } ``` ```typescript TypeScript // Process tool use in Claude's response for (const block of response.content) { if (block.type === "tool_use") { // Execute the tool based on command const result = handleEditorTool(block); // Return result to Claude const toolResult = { type: "tool_result", tool_use_id: block.id, content: result }; } } ``` ```csharp C# // Process tool use in Claude's response foreach (var block in response.Content) { if (block.TryPickToolUse(out var toolUse)) { var result = HandleEditorTool(toolUse.Input); var toolResult = new ToolResultBlockParam { ToolUseID = toolUse.ID, Content = result, }; } } ``` ```go Go // Process tool use in Claude's response for _, block := range response.Content { if block.Type == "tool_use" { var input map[string]any if err := json.Unmarshal(block.Input, &input); err != nil { log.Fatal(err) } result := handleEditorTool(input) toolResult := anthropic.NewToolResultBlock(block.ID, result, false) // ... } } ``` ```java Java // Process tool use in Claude's response for (var block : response.content()) { if (block.type().equals("tool_use")) { // Execute the tool based on command var result = handleEditorTool(block); // Return result to Claude var toolResult = Map.of( "type", "tool_result", "tool_use_id", block.id(), "content", result ); } } ``` ```php PHP // Process tool use in Claude's response foreach ($response->content as $block) { if ($block->type === 'tool_use') { // Execute the tool based on command $result = handle_editor_tool($block->input); // Return result to Claude $toolResult = [ 'type' => 'tool_result', 'tool_use_id' => $block->id, 'content' => $result, ]; } } ``` ```ruby Ruby # Process tool use in Claude's response tool_results = response.content.filter_map do |block| next unless block.type == :tool_use {type: "tool_result", tool_use_id: block.id, content: handle_editor_tool(block.input)} end ``` When implementing the text editor tool, keep in mind: 1. **Security:** The tool has access to your local filesystem, so implement proper security measures. 2. **Backup:** Always create backups before allowing edits to important files. 3. **Validation:** Validate all inputs to prevent unintended changes. 4. **Unique matching:** Make sure replacements match exactly one location to avoid unintended edits. ### Handle errors When using the text editor tool, various errors may occur. Here is guidance on how to handle them: If Claude tries to view or modify a file that doesn't exist, return an appropriate error message in the `tool_result`: ```json { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_01A09q90qw90lq917835lq9", "content": "Error: File not found", "is_error": true } ] } ``` If Claude's `str_replace` command matches multiple locations in the file, return an appropriate error message: ```json { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_01A09q90qw90lq917835lq9", "content": "Error: Found 3 matches for replacement text. Please provide more context to make a unique match.", "is_error": true } ] } ``` If Claude's `str_replace` command doesn't match any text in the file, return an appropriate error message: ```json { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_01A09q90qw90lq917835lq9", "content": "Error: No match found for replacement. Please check your text and try again.", "is_error": true } ] } ``` If there are permission issues with creating, reading, or modifying files, return an appropriate error message: ```json { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_01A09q90qw90lq917835lq9", "content": "Error: Permission denied. Cannot write to file.", "is_error": true } ] } ``` ### Follow implementation best practices When asking Claude to fix or modify code, be specific about what files need to be examined or what issues need to be addressed. Clear context helps Claude identify the right files and make appropriate changes. **Less helpful prompt:** "Can you fix my code?" **Better prompt:** "There's a syntax error in my primes.py file that prevents it from running. Can you fix it?" Specify file paths clearly when needed, especially if you're working with multiple files or files in different directories. **Less helpful prompt:** "Review my helper file" **Better prompt:** "Can you check my utils/helpers.py file for any performance issues?" Implement a backup system in your application that creates copies of files before allowing Claude to edit them, especially for important or production code. ```python Python def backup_file(file_path): """Create a backup of a file before editing.""" backup_path = f"{file_path}.backup" if os.path.exists(file_path): with open(file_path, "r") as src, open(backup_path, "w") as dst: dst.write(src.read()) ``` ```typescript TypeScript async function backupFile(filePath: string): Promise { const backupPath = `${filePath}.backup`; try { await access(filePath); await copyFile(filePath, backupPath); } catch { // File does not exist; nothing to back up } } ``` ```csharp C# static void BackupFile(string filePath) { var backupPath = $"{filePath}.backup"; if (File.Exists(filePath)) { File.Copy(filePath, backupPath, overwrite: true); } } ``` ```go Go func backupFile(filePath string) error { backupPath := filePath + ".backup" data, err := os.ReadFile(filePath) if err != nil { if os.IsNotExist(err) { return nil } return err } return os.WriteFile(backupPath, data, 0o644) } ``` ```java Java static void backupFile(String filePath) throws IOException { Path source = Path.of(filePath); Path backupPath = Path.of(filePath + ".backup"); if (Files.exists(source)) { Files.copy(source, backupPath, StandardCopyOption.REPLACE_EXISTING); } } ``` ```php PHP function backup_file(string $filePath): void { $backupPath = $filePath . '.backup'; if (file_exists($filePath)) { copy($filePath, $backupPath); } } ``` ```ruby Ruby def backup_file(file_path) backup_path = "#{file_path}.backup" FileUtils.cp(file_path, backup_path) if File.exist?(file_path) end ``` The `str_replace` command requires an exact match for the text to be replaced. Your application should ensure that there is exactly one match for the old text or provide appropriate error messages. ```python Python def safe_replace(file_path, old_text, new_text): """Replace text only if there's exactly one match.""" with open(file_path, "r") as f: content = f.read() count = content.count(old_text) if count == 0: return "Error: No match found" elif count > 1: return f"Error: Found {count} matches" else: new_content = content.replace(old_text, new_text) with open(file_path, "w") as f: f.write(new_content) return "Successfully replaced text" ``` ```typescript TypeScript async function safeReplace( filePath: string, oldText: string, newText: string ): Promise { const content = await readFile(filePath, "utf8"); const count = content.split(oldText).length - 1; if (count === 0) { return "Error: No match found"; } else if (count > 1) { return `Error: Found ${count} matches`; } else { const newContent = content.replace(oldText, newText); await writeFile(filePath, newContent, "utf8"); return "Successfully replaced text"; } } ``` ```csharp C# static string SafeReplace(string filePath, string oldText, string newText) { var content = File.ReadAllText(filePath); var count = content.Split(oldText).Length - 1; if (count == 0) { return "Error: No match found"; } else if (count > 1) { return $"Error: Found {count} matches"; } else { var newContent = content.Replace(oldText, newText); File.WriteAllText(filePath, newContent); return "Successfully replaced text"; } } ``` ```go Go func safeReplace(filePath, oldText, newText string) string { data, err := os.ReadFile(filePath) if err != nil { return fmt.Sprintf("Error: %v", err) } content := string(data) count := strings.Count(content, oldText) if count == 0 { return "Error: No match found" } else if count > 1 { return fmt.Sprintf("Error: Found %d matches", count) } newContent := strings.Replace(content, oldText, newText, 1) if err := os.WriteFile(filePath, []byte(newContent), 0o644); err != nil { return fmt.Sprintf("Error: %v", err) } return "Successfully replaced text" } ``` ```java Java static String safeReplace(String filePath, String oldText, String newText) throws IOException { String content = Files.readString(Path.of(filePath)); int count = content.split(Pattern.quote(oldText), -1).length - 1; if (count == 0) { return "Error: No match found"; } else if (count > 1) { return "Error: Found " + count + " matches"; } else { String newContent = content.replace(oldText, newText); Files.writeString(Path.of(filePath), newContent); return "Successfully replaced text"; } } ``` ```php PHP function safe_replace(string $filePath, string $oldText, string $newText): string { $content = file_get_contents($filePath); $count = substr_count($content, $oldText); if ($count === 0) { return 'Error: No match found'; } elseif ($count > 1) { return "Error: Found {$count} matches"; } else { $newContent = str_replace($oldText, $newText, $content); file_put_contents($filePath, $newContent); return 'Successfully replaced text'; } } ``` ```ruby Ruby def safe_replace(file_path, old_text, new_text) content = File.read(file_path) count = content.scan(old_text).length if count == 0 "Error: No match found" elsif count > 1 "Error: Found #{count} matches" else new_content = content.sub(old_text) { new_text } File.write(file_path, new_content) "Successfully replaced text" end end ``` After Claude makes changes to a file, verify the changes by running tests or checking that the code still works as expected. ```python Python def verify_changes(file_path): """Run tests or checks after making changes.""" try: # For Python files, check for syntax errors if file_path.endswith(".py"): import ast with open(file_path, "r") as f: ast.parse(f.read()) return "Syntax check passed" except Exception as e: return f"Verification failed: {str(e)}" ``` ```typescript TypeScript function verifyChanges(filePath: string): string { try { // For Python files, check for syntax errors if (filePath.endsWith(".py")) { execFileSync("python3", ["-m", "py_compile", filePath]); return "Syntax check passed"; } return "No checks defined for this file type"; } catch (err) { return `Verification failed: ${err}`; } } ``` ```csharp C# static string VerifyChanges(string filePath) { try { // For Python files, check for syntax errors if (filePath.EndsWith(".py")) { var psi = new ProcessStartInfo("python3") { RedirectStandardError = true, }; psi.ArgumentList.Add("-m"); psi.ArgumentList.Add("py_compile"); psi.ArgumentList.Add(filePath); using var proc = Process.Start(psi)!; proc.WaitForExit(); if (proc.ExitCode != 0) { return $"Verification failed: {proc.StandardError.ReadToEnd()}"; } return "Syntax check passed"; } return "No checks defined for this file type"; } catch (Exception e) { return $"Verification failed: {e.Message}"; } } ``` ```go Go func verifyChanges(filePath string) string { // For Python files, check for syntax errors if strings.HasSuffix(filePath, ".py") { cmd := exec.Command("python3", "-m", "py_compile", filePath) if out, err := cmd.CombinedOutput(); err != nil { return fmt.Sprintf("Verification failed: %v: %s", err, out) } return "Syntax check passed" } return "No checks defined for this file type" } ``` ```java Java static String verifyChanges(String filePath) { try { // For Python files, check for syntax errors if (filePath.endsWith(".py")) { Process proc = new ProcessBuilder("python3", "-m", "py_compile", filePath) .redirectErrorStream(true) .start(); if (proc.waitFor() != 0) { return "Verification failed: " + new String(proc.getInputStream().readAllBytes()); } return "Syntax check passed"; } return "No checks defined for this file type"; } catch (IOException | InterruptedException e) { return "Verification failed: " + e.getMessage(); } } ``` ```php PHP function verify_changes(string $filePath): string { // For Python files, check for syntax errors if (str_ends_with($filePath, '.py')) { exec('python3 -m py_compile ' . escapeshellarg($filePath) . ' 2>&1', $output, $exitCode); if ($exitCode !== 0) { return 'Verification failed: ' . implode("\n", $output); } return 'Syntax check passed'; } return 'No checks defined for this file type'; } ``` ```ruby Ruby def verify_changes(file_path) # For Python files, check for syntax errors if file_path.end_with?(".py") if system("python3", "-m", "py_compile", file_path) "Syntax check passed" else "Verification failed: syntax error in #{file_path}" end else "No checks defined for this file type" end end ``` *** ## Pricing and token usage The text editor tool uses the same pricing structure as other tools used with Claude. It follows the standard input and output token pricing based on the Claude model you're using. In addition to the base tokens, the following additional input tokens are needed for the text editor tool: | Tool | Additional input tokens | | ----------------------------------- | ----------------------- | | `text_editor_20250429` (Claude 4.x) | 700 tokens | For more detailed information about tool pricing, see [Tool use pricing](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview#pricing). ## Integrate the text editor tool with other tools You can use the text editor tool alongside other Claude tools. When combining tools, ensure you: * Match the tool version with the model you're using * Account for the additional token usage for all tools included in your request ## Change log | Date | Version | Changes | | ---------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | July 28, 2025 | `text_editor_20250728` | Release of an updated text editor tool that fixes some issues and adds an optional `max_characters` parameter. It is otherwise identical to `text_editor_20250429`. | | April 29, 2025 | `text_editor_20250429` | Release of the text editor tool for Claude 4. This version removes the `undo_edit` command but maintains all other capabilities. The tool name has been updated to reflect its str\_replace-based architecture. | | March 13, 2025 | `text_editor_20250124` | Introduction of standalone text editor tool documentation. This version is optimized for Claude Sonnet 3.7 but has identical capabilities to the previous version. | | October 22, 2024 | `text_editor_20241022` | Initial release of the text editor tool with Claude Sonnet 3.5 (retired; see [Model deprecations](https://platform.claude.com/docs/en/about-claude/model-deprecations)). Provides capabilities for viewing, creating, and editing files through the `view`, `create`, `str_replace`, `insert`, and `undo_edit` commands. | ## Next steps Here are some ideas for how to use the text editor tool in more convenient and powerful ways: * **Integrate with your development workflow**: Build the text editor tool into your development tools or IDE * **Create a code review system**: Have Claude review your code and make improvements * **Build a debugging assistant**: Create a system where Claude can help you diagnose and fix issues in your code * **Implement file format conversion**: Let Claude help you convert files from one format to another * **Automate documentation**: Set up workflows for Claude to automatically document your code The text editor tool enables Claude to work directly with your code base, supporting workflows from debugging to automated documentation. Learn how to implement tool workflows for use with Claude. Execute shell commands with Claude. --- title: Tool runner (SDK) url: https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-runner description: Use the SDK's tool runner to handle the agentic loop, error wrapping, and type safety automatically. --- The tool runner handles the agentic loop, error wrapping, and type safety so you don't have to. When you need human-in-the-loop approval, custom logging, or conditional execution, use the [manual loop](https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls) instead. Instead of manually handling tool calls, tool results, and conversation management, the tool runner automatically: * Runs tools when Claude calls them * Handles the request/response cycle * Manages conversation state * Provides type safety and validation The tool runner is in beta and available in the [Python SDK](https://github.com/anthropics/anthropic-sdk-python/blob/main/tools.md), [TypeScript SDK](https://github.com/anthropics/anthropic-sdk-typescript/blob/main/helpers.md#tool-helpers), [C# SDK](https://github.com/anthropics/anthropic-sdk-csharp/blob/main/examples/ToolRunnerExample/Program.cs), [Go SDK](https://github.com/anthropics/anthropic-sdk-go/blob/main/tools.md), [Java SDK](https://github.com/anthropics/anthropic-sdk-java/blob/main/anthropic-java-example/src/main/java/com/anthropic/example/BetaToolRunnerExample.java), [PHP SDK](https://github.com/anthropics/anthropic-sdk-php/blob/main/examples/beta/beta_tool_runner.php), and [Ruby SDK](https://github.com/anthropics/anthropic-sdk-ruby/blob/main/helpers.md#3-auto-looping-tool-runner-beta). ## Basic usage Define tools using the SDK helpers, then use the tool runner to run them. Depending on the SDK's tool signature, a tool returns its result as a string or as content blocks (text, image, or document blocks), so a tool can return multimodal results. A returned string becomes a single text content block. To return structured data, such as a JSON object or a number, encode it as a string first. Use the `@beta_tool` decorator to define tools with type hints and docstrings. If you're using the async client, replace `@beta_tool` with `@beta_async_tool` and define the function with `async def`. ```python import json from anthropic import Anthropic, beta_tool client = Anthropic() @beta_tool def get_weather(location: str, unit: str = "fahrenheit") -> str: """Get the current weather in a given location. Args: location: The city and state, e.g. San Francisco, CA unit: Temperature unit, either 'celsius' or 'fahrenheit' """ return json.dumps({"temperature": "20°C", "condition": "Sunny"}) @beta_tool def calculate_sum(a: int, b: int) -> str: """Add two numbers together. Args: a: First number b: Second number """ return str(a + b) runner = client.beta.messages.tool_runner( model="claude-opus-5", max_tokens=1024, tools=[get_weather, calculate_sum], messages=[ { "role": "user", "content": "What's the weather like in Paris? Also, what's 15 + 27?", } ], ) for message in runner: print(message) ``` The `@beta_tool` decorator inspects the function arguments and docstring to derive the JSON schema for you. Use `betaZodTool()` for type-safe tool definitions with Zod validation, or `betaTool()` for JSON Schema-based definitions. TypeScript offers two approaches for defining tools: **Using Zod (recommended)** - Use `betaZodTool()` for type-safe tool definitions with Zod validation (requires Zod 3.25.0 or higher): ```typescript import Anthropic from "@anthropic-ai/sdk"; import { betaZodTool } from "@anthropic-ai/sdk/helpers/beta/zod"; import { z } from "zod"; const client = new Anthropic(); const getWeatherTool = betaZodTool({ name: "get_weather", description: "Get the current weather in a given location", inputSchema: z.object({ location: z.string().describe("The city and state, e.g. San Francisco, CA"), unit: z.enum(["celsius", "fahrenheit"]).default("fahrenheit").describe("Temperature unit") }), run: async (input) => { return JSON.stringify({ temperature: "20°C", condition: "Sunny" }); } }); const finalMessage = await client.beta.messages.toolRunner({ model: "claude-opus-5", max_tokens: 1024, tools: [getWeatherTool], messages: [{ role: "user", content: "What's the weather like in Paris?" }] }); for (const block of finalMessage.content) { if (block.type === "text") { console.log(block.text); } } ``` **Using JSON Schema** - Use `betaTool()` for type-safe tool definitions without Zod: The input generated by Claude is not validated at runtime. Perform validation inside the `run` function if needed. ```typescript import Anthropic from "@anthropic-ai/sdk"; import { betaTool } from "@anthropic-ai/sdk/helpers/beta/json-schema"; const client = new Anthropic(); const calculateSumTool = betaTool({ name: "calculate_sum", description: "Add two numbers together", inputSchema: { type: "object", properties: { a: { type: "number", description: "First number" }, b: { type: "number", description: "Second number" } }, required: ["a", "b"] }, run: async (input) => { return String(input.a + input.b); } }); const finalMessage = await client.beta.messages.toolRunner({ model: "claude-opus-5", max_tokens: 1024, tools: [calculateSumTool], messages: [{ role: "user", content: "What's 15 + 27?" }] }); for (const block of finalMessage.content) { if (block.type === "text") { console.log(block.text); } } ``` Define each tool as a `BetaRunnableTool`, providing a `Definition` with a JSON schema and a `Run` delegate that runs when Claude calls the tool. ```csharp using System.Text.Json; using Anthropic; using Anthropic.Helpers.Beta; using Anthropic.Models.Beta.Messages; using MessageCreateParams = Anthropic.Models.Beta.Messages.MessageCreateParams; using InputSchema = Anthropic.Models.Beta.Messages.InputSchema; using Role = Anthropic.Models.Beta.Messages.Role; using Model = Anthropic.Models.Messages.Model; var client = new AnthropicClient(); var getWeatherTool = new BetaRunnableTool { Name = "get_weather", Definition = new BetaTool { Name = "get_weather", Description = "Get the current weather in a given location.", InputSchema = new InputSchema { Properties = new Dictionary { ["location"] = JsonSerializer.SerializeToElement( new { type = "string", description = "The city and state, e.g. San Francisco, CA" } ), }, Required = ["location"], }, }, Run = (toolUse, _) => { var location = toolUse.Input["location"].GetString(); return Task.FromResult( $"Weather in {location}: 20°C, sunny" ); }, }; var calculateSumTool = new BetaRunnableTool { Name = "calculate_sum", Definition = new BetaTool { Name = "calculate_sum", Description = "Add two numbers together.", InputSchema = new InputSchema { Properties = new Dictionary { ["a"] = JsonSerializer.SerializeToElement(new { type = "number" }), ["b"] = JsonSerializer.SerializeToElement(new { type = "number" }), }, Required = ["a", "b"], }, }, Run = (toolUse, _) => { var a = toolUse.Input["a"].GetDouble(); var b = toolUse.Input["b"].GetDouble(); return Task.FromResult($"{a + b}"); }, }; var runner = client.Beta.Messages.ToolRunner( new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [ new() { Role = Role.User, Content = "What's the weather like in Paris? Also, what's 15 + 27?", }, ], }, [getWeatherTool, calculateSumTool] ); await foreach (var message in runner) { Console.WriteLine(message); } ``` Define a tool with `toolrunner.NewBetaToolFromJSONSchema`. The handler's input type is a struct with `jsonschema:` tags. The SDK reflects on it to generate the JSON schema. ```go package main import ( "context" "fmt" "log" "github.com/anthropics/anthropic-sdk-go" "github.com/anthropics/anthropic-sdk-go/toolrunner" ) type GetWeatherInput struct { Location string `json:"location" jsonschema:"required,description=The city and state, e.g. San Francisco, CA"` Unit string `json:"unit,omitempty" jsonschema:"enum=celsius,enum=fahrenheit,description=Temperature unit"` } type CalculateSumInput struct { A int `json:"a" jsonschema:"required,description=First number"` B int `json:"b" jsonschema:"required,description=Second number"` } func main() { client := anthropic.NewClient() ctx := context.Background() getWeather, err := toolrunner.NewBetaToolFromJSONSchema( "get_weather", "Get the current weather in a given location.", func(ctx context.Context, input GetWeatherInput) (anthropic.BetaToolResultBlockParamContentUnion, error) { return anthropic.BetaToolResultBlockParamContentUnion{ OfText: &anthropic.BetaTextBlockParam{Text: "20°C, Sunny"}, }, nil }, ) if err != nil { log.Fatal(err) } calculateSum, err := toolrunner.NewBetaToolFromJSONSchema( "calculate_sum", "Add two numbers together.", func(ctx context.Context, input CalculateSumInput) (anthropic.BetaToolResultBlockParamContentUnion, error) { return anthropic.BetaToolResultBlockParamContentUnion{ OfText: &anthropic.BetaTextBlockParam{Text: fmt.Sprintf("%d", input.A+input.B)}, }, nil }, ) if err != nil { log.Fatal(err) } runner := client.Beta.Messages.NewToolRunner( []anthropic.BetaTool{getWeather, calculateSum}, anthropic.BetaToolRunnerParams{ BetaMessageNewParams: anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock( "What's the weather like in Paris? Also, what's 15 + 27?", )), }, }, }, ) for message, err := range runner.All(ctx) { if err != nil { log.Fatal(err) } fmt.Println(message) } } ``` The `jsonschema:` struct tags generate the input schema. For example, `CalculateSumInput` becomes: ```json { "name": "calculate_sum", "description": "Add two numbers together.", "input_schema": { "type": "object", "properties": { "a": { "type": "integer", "description": "First number" }, "b": { "type": "integer", "description": "Second number" } }, "required": ["a", "b"] } } ``` Define each tool as a class implementing `Supplier`. Annotate the class with `@JsonClassDescription` for the tool description, and each public field with `@JsonPropertyDescription` for parameter descriptions. The SDK derives the JSON schema, tool name (snake-cased class name), and input parsing from the class, and marks the tool with `strict: true` ([strict tool use](https://platform.claude.com/docs/en/agents-and-tools/tool-use/strict-tool-use)). ```java import com.anthropic.client.AnthropicClient; import com.anthropic.client.okhttp.AnthropicOkHttpClient; import com.anthropic.helpers.BetaToolRunner; import com.anthropic.models.beta.messages.BetaMessage; import com.anthropic.models.beta.messages.MessageCreateParams; import com.anthropic.models.messages.Model; import com.fasterxml.jackson.annotation.JsonClassDescription; import com.fasterxml.jackson.annotation.JsonPropertyDescription; import java.util.function.Supplier; @JsonClassDescription("Get the current weather in a given location") static class GetWeather implements Supplier { @JsonPropertyDescription("The city and state, e.g. San Francisco, CA") public String location; @JsonPropertyDescription("Temperature unit, either 'celsius' or 'fahrenheit'") public String unit; @Override public String get() { return "{\"temperature\": \"20°C\", \"condition\": \"Sunny\"}"; } } @JsonClassDescription("Add two numbers together") static class CalculateSum implements Supplier { @JsonPropertyDescription("First number") public double a; @JsonPropertyDescription("Second number") public double b; @Override public String get() { return String.valueOf(a + b); } } void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); BetaToolRunner runner = client.beta() .messages() .toolRunner(MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .addBeta("structured-outputs-2025-11-13") .addUserMessage("What's the weather like in Paris? Also, what's 15 + 27?") .addTool(GetWeather.class) .addTool(CalculateSum.class) .build()); for (BetaMessage message : runner) { IO.println(message); } } ``` The class name `CalculateSum` becomes the tool name `calculate_sum`, and the SDK generates a JSON schema from the annotated fields: ```json { "name": "calculate_sum", "description": "Add two numbers together", "input_schema": { "type": "object", "properties": { "a": { "description": "First number", "type": "number" }, "b": { "description": "Second number", "type": "number" } }, "required": ["a", "b"], "additionalProperties": false }, "strict": true } ``` Define each tool as a `BetaRunnableTool` that pairs the tool's JSON schema definition with a closure that runs it. ```php 'get_weather', 'description' => 'Get the current weather in a given location.', 'input_schema' => [ 'type' => 'object', 'properties' => [ 'location' => [ 'type' => 'string', 'description' => 'The city and state, e.g. San Francisco, CA', ], 'unit' => [ 'type' => 'string', 'enum' => ['celsius', 'fahrenheit'], ], ], 'required' => ['location'], ], ], run: fn (array $input): string => json_encode([ 'temperature' => '20°C', 'condition' => 'Sunny', ]), ); $calculateSum = new BetaRunnableTool( definition: [ 'name' => 'calculate_sum', 'description' => 'Add two numbers together.', 'input_schema' => [ 'type' => 'object', 'properties' => [ 'a' => ['type' => 'number', 'description' => 'First number'], 'b' => ['type' => 'number', 'description' => 'Second number'], ], 'required' => ['a', 'b'], ], ], run: fn (array $input): string => (string) ($input['a'] + $input['b']), ); $runner = $client->beta->messages->toolRunner( maxTokens: 1024, messages: [ ['role' => 'user', 'content' => "What's the weather like in Paris? Also, what's 15 + 27?"], ], model: Model::CLAUDE_OPUS_5, tools: [$getWeather, $calculateSum], ); foreach ($runner as $message) { foreach ($message->content as $block) { if ($block->type === 'text') { echo $block->text, "\n"; } elseif ($block->type === 'tool_use') { echo "[Tool call: {$block->name}]\n"; } } } ``` Use the `Anthropic::BaseTool` class to define tools with typed input schemas. ```ruby require "anthropic" # Initialize client client = Anthropic::Client.new # Define input schema class GetWeatherInput < Anthropic::BaseModel required :location, String, doc: "The city and state, e.g. San Francisco, CA" optional :unit, Anthropic::InputSchema::EnumOf["celsius", "fahrenheit"], doc: "Temperature unit" end # Define tool class GetWeather < Anthropic::BaseTool doc "Get the current weather in a given location" input_schema GetWeatherInput def call(input) # In a full implementation, you'd call a weather API here JSON.generate({temperature: "20°C", condition: "Sunny"}) end end class CalculateSumInput < Anthropic::BaseModel required :a, Integer, doc: "First number" required :b, Integer, doc: "Second number" end class CalculateSum < Anthropic::BaseTool doc "Add two numbers together" input_schema CalculateSumInput def call(input) (input.a + input.b).to_s end end # Use the tool runner runner = client.beta.messages.tool_runner( model: "claude-opus-5", max_tokens: 1024, tools: [GetWeather.new, CalculateSum.new], messages: [ {role: "user", content: "What's the weather like in Paris? Also, what's 15 + 27?"} ] ) runner.each_message do |message| message.content.each do |block| puts block.text if block.type == :text end end ``` The `Anthropic::BaseTool` class uses the `doc` method for the tool description and `input_schema` to define the expected parameters. The SDK automatically converts this to the appropriate JSON schema format. ## Iterating over the tool runner The tool runner is an iterable that yields messages from Claude. On each iteration, the runner checks whether Claude requested a tool use. If so, it runs the tool and sends the result back to Claude automatically, then yields the next message from Claude to continue your loop. You can end the loop at any iteration with a `break` statement. The runner loops until Claude returns a message without a tool use, or until it reaches `max_iterations` if you set it. If you don't need intermediate messages, you can get the final message directly: Use `runner.until_done()` to get the final message. ```python client = anthropic.Anthropic() # ... runner = client.beta.messages.tool_runner( model="claude-opus-5", max_tokens=1024, tools=[get_weather, calculate_sum], messages=[ { "role": "user", "content": "What's the weather like in Paris? Also, what's 15 + 27?", } ], ) final_message = runner.until_done() for block in final_message.content: if block.type == "text": print(block.text) ``` `await` the runner to get the final message. ```typescript const client = new Anthropic(); // ... const runner = client.beta.messages.toolRunner({ model: "claude-opus-5", max_tokens: 1024, tools: [getWeatherTool], messages: [{ role: "user", content: "What's the weather like in Paris?" }] }); const finalMessage = await runner; for (const block of finalMessage.content) { if (block.type === "text") { console.log(block.text); } } ``` Use `runner.RunUntilDoneAsync()` to get the final message. ```csharp var client = new AnthropicClient(); // ... var runner = client.Beta.Messages.ToolRunner( new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [ new() { Role = Role.User, Content = "What's the weather like in Paris?", }, ], }, [getWeatherTool] ); var finalMessage = await runner.RunUntilDoneAsync(); foreach (var block in finalMessage.Content) { if (block.TryPickText(out var textBlock)) { Console.WriteLine(textBlock.Text); } } ``` Use `runner.RunToCompletion(ctx)` to get the final message. ```go client := anthropic.NewClient() ctx := context.Background() // ... runner := client.Beta.Messages.NewToolRunner( []anthropic.BetaTool{getWeather}, anthropic.BetaToolRunnerParams{ BetaMessageNewParams: anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock( "What's the weather like in Paris?", )), }, }, }, ) finalMessage, err := runner.RunToCompletion(ctx) if err != nil { log.Fatal(err) } for _, block := range finalMessage.Content { if textBlock, ok := block.AsAny().(anthropic.BetaTextBlock); ok { fmt.Println(textBlock.Text) } } ``` The Java SDK has no `until_done()` shortcut. Iterate to exhaustion and keep the last message. ```java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); BetaToolRunner runner = client.beta() .messages() .toolRunner(MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .addBeta("structured-outputs-2025-11-13") .addUserMessage("What's the weather like in Paris? Also, what's 15 + 27?") .addTool(GetWeather.class) .addTool(CalculateSum.class) .build()); BetaMessage finalMessage = null; for (BetaMessage message : runner) { finalMessage = message; } for (BetaContentBlock block : finalMessage.content()) { block.text().ifPresent(textBlock -> IO.println(textBlock.text())); } ``` Use `runUntilDone()` to get the final message. ```php $client = new Client(); // ... $runner = $client->beta->messages->toolRunner( maxTokens: 1024, messages: [ ['role' => 'user', 'content' => "What's the weather like in Paris? Also, what's 15 + 27?"], ], model: Model::CLAUDE_OPUS_5, tools: [$getWeather, $calculateSum], ); $finalMessage = $runner->runUntilDone(); foreach ($finalMessage->content as $block) { if ($block->type === 'text') { echo $block->text, "\n"; } } ``` Use `runner.run_until_finished` to get all messages. ```ruby client = Anthropic::Client.new # ... runner = client.beta.messages.tool_runner( model: "claude-opus-5", max_tokens: 1024, tools: [GetWeather.new, CalculateSum.new], messages: [ {role: "user", content: "What's the weather like in Paris? Also, what's 15 + 27?"} ] ) all_messages = runner.run_until_finished all_messages.each { |msg| puts msg.content } ``` ## Advanced usage Within the loop, you can read each response message and modify the runner's state before the next API call. Each iteration follows this lifecycle: 1. The runner sends a request to the Messages API with its current state. 2. The runner yields the response message to your loop body. 3. Your loop body runs. You can read the message and optionally modify the runner's state. 4. When your loop body returns, the runner checks whether you modified its message history. * **If you did not modify message history:** If the message contains tool calls, the runner appends the assistant message and the tool results, then continues. If there are no tool calls, the loop exits. * **If you modified message history:** The runner skips its automatic append and uses your state unchanged. See [Taking over message history](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-runner#taking-over-message-history). ```mermaid sequenceDiagram participant U as Your code participant TR as ToolRunner participant API as Messages API loop For each iteration TR->>API: Send request with current state API-->>TR: Response message TR-->>U: Yield message note over U: Your loop body runs U->>TR: Resume alt Message history unchanged TR->>TR: If tool calls, append assistant
message + tool results and continue.
If none, exit the loop else Message history changed TR->>TR: Use your state unchanged end end ``` ### Taking over message history By default, the runner manages conversation state for you: after each tool-call turn, it appends the assistant message and any tool results to its own message history. You take over message history when you want to retry a turn (discard the response and resend), inject a follow-up message, or build the tool result yourself. You take over by modifying the runner's messages from inside the loop body. The exact method depends on the SDK. See the per-language tabs that follow. When you take over for an iteration, the runner does not append the assistant message or tool results from that turn. You become responsible for keeping the conversation valid: append the assistant message and a tool result yourself (if you want the turn to count), modify state conditionally so the loop can still exit when there are no tool calls, and pass `max_iterations` to bound the loop. All seven SDKs support `max_iterations`. Use `generate_tool_call_response()` to inspect or compute the tool result. Calling `append_messages()` inside the loop tells the runner you're managing history yourself, so include the assistant message and tool result in what you append. ```python runner = client.beta.messages.tool_runner( model="claude-opus-5", max_tokens=1024, max_iterations=10, tools=[get_weather], messages=[{"role": "user", "content": "What's the weather in San Francisco?"}], ) for message in runner: tool_response = runner.generate_tool_call_response() if tool_response is not None: # append_messages() flags state as modified, so the runner skips its # automatic append for this iteration. Append the assistant message and # tool result yourself, plus any follow-up. runner.append_messages( message, tool_response, {"role": "user", "content": "Please be concise."}, ) # When there's no tool call, leave state untouched so the loop exits. ``` To change request parameters such as `max_tokens` without taking over message history, use `set_messages_params()`. The runner still appends the assistant message and tool result automatically. ```python for message in runner: runner.set_messages_params(lambda params: {**params, "max_tokens": 2048}) ``` Use `runner.params` to read the current request parameters and `setMessagesParams()` to replace them. Calling `setMessagesParams()` or `pushMessages()` inside the loop tells the runner you're managing state yourself: the assistant message and tool result from this iteration are dropped, and the next request goes out with your state. The following example retries a truncated response with a larger `max_tokens` budget. ```typescript const runner = client.beta.messages.toolRunner({ model: "claude-opus-5", max_tokens: 1024, max_iterations: 10, tools: [getWeatherTool], messages: [ { role: "user", content: "Give me a detailed weather report for every major US city." } ] }); const MAX_TOKEN_CEILING = 8192; for await (const message of runner) { if (message.stop_reason === "max_tokens") { const current = runner.params.max_tokens; if (current >= MAX_TOKEN_CEILING) { console.warn(`Hit ceiling (${MAX_TOKEN_CEILING}); stopping.`); break; } const doubled = Math.min(current * 2, MAX_TOKEN_CEILING); console.log(`Response truncated at ${current} tokens; retrying with ${doubled}.`); // Bump the budget. setMessagesParams() flags state as modified, so the // runner does NOT append the truncated message. The next iteration retries // the same turn with the larger budget. runner.setMessagesParams((params) => ({ ...params, max_tokens: doubled })); } // Otherwise leave state untouched so the runner auto-appends and continues. } ``` Calling `SetParams()` or `PushMessages()` flags state as modified, which causes the runner to skip its auto-append for that turn. The C# runner still runs the matched tools for that turn and discards their auto-built results, so a tool you also run yourself inside the loop body runs twice unless you account for it. When you take over, push the assistant message and a tool result yourself. Otherwise the conversation won't make forward progress. The C# runner always exits when a response has no tool calls, so condition any state mutation on the presence of a `tool_use` block. ```csharp var runner = client.Beta.Messages.ToolRunner( new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [new() { Role = Role.User, Content = "What's the weather in San Francisco?" }], }, [getWeatherTool], maxIterations: 10 ); await foreach (var message in runner) { var toolUseBlock = message .Content.Select(block => block.TryPickToolUse(out var toolUse) ? toolUse : null) .FirstOrDefault(toolUse => toolUse is not null); if (toolUseBlock is null) { // No tool call: leave state untouched so the loop exits normally. continue; } // Run the tool yourself and build the result block. var toolResult = new BetaToolResultBlockParam(toolUseBlock.ID) { Content = await getWeatherTool.ExecuteAsync(toolUseBlock, default), }; // PushMessages() flags state as modified; the runner skips its auto-append. // Supply the assistant turn and the tool result yourself, then add a follow-up. runner.PushMessages( new() { Role = Role.Assistant, Content = new BetaMessageParamContent( JsonSerializer.SerializeToElement( message.Content.Select(block => block.Json).ToArray() ) ), }, new() { Role = Role.User, Content = new List { toolResult }, }, new() { Role = Role.User, Content = "Please be concise in your response." } ); } ``` The Go runner exposes parameters as a public `Params` field. Modifying `runner.Params` between calls to `NextMessage(ctx)` applies to the next API request. Unlike other SDKs, the Go runner always appends the assistant message and tool results unconditionally. Modifying `Params` does not suppress that step. ```go runner := client.Beta.Messages.NewToolRunner( []anthropic.BetaTool{getWeather}, anthropic.BetaToolRunnerParams{ BetaMessageNewParams: anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock( "What's the weather in San Francisco?", )), }, }, MaxIterations: 10, }, ) for { message, err := runner.NextMessage(ctx) if err != nil { log.Fatal(err) } if message == nil { break // conversation complete } // The Go runner always appends the assistant message and tool results. // Param changes here apply to the next iteration. runner.Params.MaxTokens = 2048 } ``` Use `runner.params()` to read the current parameters and `runner.setNextParams()` to replace them for the next iteration. When you call `setNextParams()` inside the loop, the runner skips its automatic append. The just-yielded message is discarded, and the next iteration sends your new params unchanged. The following example retries a turn that hit the token limit by doubling `max_tokens`. Mutating only on the `max_tokens` branch keeps the loop converging: turns that complete normally fall through, and the runner auto-appends and exits when there are no more tool calls. ```java BetaToolRunner runner = client.beta() .messages() .toolRunner(ToolRunnerCreateParams.builder() .initialMessageParams(MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .addBeta("structured-outputs-2025-11-13") .addUserMessage("Give me a detailed weather report for every major US city.") .addTool(GetWeather.class) .build()) .maxIterations(10L) .build()); long ceiling = 8192; for (BetaMessage message : runner) { if (BetaStopReason.MAX_TOKENS.equals(message.stopReason().orElse(null))) { long current = runner.params().maxTokens(); if (current >= ceiling) { IO.println("Hit ceiling (" + ceiling + "), accepting truncated response."); break; } long doubled = Math.min(current * 2, ceiling); IO.println("Response truncated at " + current + " tokens, retrying with " + doubled + "."); // Calling setNextParams() flags this turn as user-managed: the runner // does NOT auto-append the truncated message, so the next iteration // re-sends the same conversation prefix with the larger budget. runner.setNextParams(runner.params().toBuilder().maxTokens(doubled).build()); } // No mutation on a normal turn: the runner auto-appends and continues. } ``` Use `setMessagesParams()` and `pushMessages()` to modify the runner's state, and `getParams()` to read it. Calling either setter inside the loop tells the runner to skip its automatic append, so the conversation continues from your modified state instead. The following example doubles `max_tokens` and retries when a response is cut off. ```php use Anthropic\Beta\Messages\BetaStopReason; $runner = $client->beta->messages->toolRunner( maxTokens: 1024, messages: [ ['role' => 'user', 'content' => 'Give a detailed weather report for every major US city.'], ], model: Model::CLAUDE_OPUS_5, tools: [$getWeather], maxIterations: 10, ); $maxTokenCeiling = 8192; foreach ($runner as $message) { if ($message->stopReason === BetaStopReason::MAX_TOKENS->value) { $current = $runner->getParams()['maxTokens']; if ($current >= $maxTokenCeiling) { echo "Hit ceiling ({$maxTokenCeiling}), accepting truncated response.\n"; break; } $doubled = min($current * 2, $maxTokenCeiling); echo "Response truncated at {$current} tokens, retrying with {$doubled}.\n"; // Calling setMessagesParams() inside the loop tells the runner to skip // its automatic append. The truncated message is discarded; the next // iteration retries with the larger budget. // Keys are camelCase, matching the toolRunner() named parameters. $runner->setMessagesParams(['maxTokens' => $doubled]); } } ``` Use `next_message` for step-by-step control. By the time `next_message` returns, the assistant message and tool result for that turn are already appended. Use `feed_messages` to inject follow-up messages between turns, and `runner.params.update(...)` to change request parameters in place. You take over message history when, from inside an `each_message` or `each_streaming` block, you reassign `runner.params[:messages]` or call `feed_messages`. The following pattern calls `feed_messages` between `next_message` calls, which does not take over. ```ruby runner = client.beta.messages.tool_runner( model: "claude-opus-5", max_tokens: 1024, max_iterations: 10, tools: [GetWeather.new], messages: [{role: "user", content: "What's the weather in San Francisco?"}] ) # Step the runner once. The assistant message and tool result are appended # to runner.params[:messages] before next_message returns. message = runner.next_message puts message.content # Inject a follow-up before continuing. feed_messages takes a splat, not an array. runner.feed_messages({role: "user", content: "Also check Boston."}) # Change parameters in place. Reassigning runner.params[:messages] takes over # message history only when it happens inside an each_message or each_streaming block. runner.params.update(max_tokens: 2048) runner.run_until_finished ``` ### Automatic context management For long-running agentic tasks, the Python, TypeScript, and Ruby tool runners support automatic [compaction](https://platform.claude.com/docs/en/build-with-claude/context-editing#client-side-compaction-sdk), which generates summaries when token usage exceeds a threshold so the conversation can continue beyond context window limits. All three SDKs have deprecated this client-side option in favor of server-side [context editing](https://platform.claude.com/docs/en/build-with-claude/context-editing), which is available in every SDK. The Go, Java, C#, and PHP tool runners don't include client-side compaction. ### Debugging tool execution When a tool throws an exception, the tool runner catches it and returns the error to Claude as a tool result with `is_error: true`. The tool result carries the exception's message (in Python, its type and message), not the full stack trace. What the SDK logs is language-specific. The Python SDK logs the full exception, including its stack trace, through the standard `logging` module whenever a tool raises an unhandled exception. The Python, TypeScript, and Java SDKs read the `ANTHROPIC_LOG` environment variable to turn on the SDK's logging, which includes request and response detail: ```bash # Log at info level export ANTHROPIC_LOG=info # Log at debug level for more verbose output export ANTHROPIC_LOG=debug ``` The Go, Ruby, C#, and PHP SDKs don't read `ANTHROPIC_LOG`. Outside Python, no SDK logs a failed tool: to see why a tool failed, catch and log the exception inside the tool function before returning or rethrowing it. ### Intercepting tool errors By default, tool errors are passed back to Claude, which can then respond appropriately. However, you might want to detect errors and handle them differently, for example, to stop execution early or implement custom error handling. In the Python and TypeScript SDKs, use the tool response method (`generate_tool_call_response()` in Python, `generateToolResponse()` in TypeScript) to intercept tool results and check for errors before they're sent to Claude. The other SDKs don't expose that hook. Their tabs describe the closest alternative: ```python client = anthropic.Anthropic() # ... runner = client.beta.messages.tool_runner( model="claude-opus-5", max_tokens=1024, tools=[my_tool], messages=[{"role": "user", "content": "Run my_tool with the query 'hello'."}], ) for message in runner: tool_response = runner.generate_tool_call_response() if tool_response is not None: # tool_response is a dict: {"role": "user", "content": [...]} # Check if any tool result has an error for block in tool_response["content"]: if block.get("is_error"): # Option 1: Raise an exception to stop the loop raise RuntimeError(f"Tool failed: {json.dumps(block['content'])}") # Option 2: Log and continue (let Claude handle it) # logger.error(f"Tool error: {json.dumps(block['content'])}") # Process the message normally print(message.content) ``` ```typescript const client = new Anthropic(); // ... const runner = client.beta.messages.toolRunner({ model: "claude-opus-5", max_tokens: 1024, tools: [myTool], messages: [{ role: "user", content: "Run my_tool with the query 'hello'." }] }); for await (const message of runner) { const toolResultMessage = await runner.generateToolResponse(); if (toolResultMessage && typeof toolResultMessage.content !== "string") { // Check if any tool result has an error for (const block of toolResultMessage.content) { if (block.type === "tool_result" && block.is_error) { // Option 1: Throw to stop the loop throw new Error(`Tool failed: ${JSON.stringify(block.content)}`); // Option 2: Log and continue (let Claude handle it) // console.error(`Tool error: ${JSON.stringify(block.content)}`); } } } // Process the message normally console.log(message.content); } ``` The C# tool runner doesn't expose a hook for inspecting the tool result before it's sent to Claude. To control error content, throw `BetaToolError` from inside the tool body. The runner converts it to a `tool_result` with `is_error: true` and the content you supply. ```csharp var client = new AnthropicClient(); var getWeatherTool = new BetaRunnableTool { Name = "get_weather", Definition = new BetaTool { Name = "get_weather", Description = "Get the current weather in a given location.", InputSchema = new InputSchema { Properties = new Dictionary { ["location"] = JsonSerializer.SerializeToElement(new { type = "string" }), }, Required = ["location"], }, }, Run = async (toolUse, cancellationToken) => { try { return await CallExternalWeatherService( toolUse.Input["location"].GetString()!, cancellationToken ); } catch (HttpRequestException ex) { // Log here if you need to inspect the failure before Claude sees it. throw new BetaToolError($"Weather service unavailable: {ex.Message}"); } }, }; var runner = client.Beta.Messages.ToolRunner( new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [ new() { Role = Role.User, Content = "What's the weather in San Francisco?" }, ], }, [getWeatherTool] ); Console.WriteLine(await runner.RunUntilDoneAsync()); ``` Intercepting tool errors before they're sent to Claude is not currently supported in the Go SDK. The runner converts an error returned from your handler into a tool result with `is_error: true` internally. To customize the error content, catch the error inside your handler and return a result instead of returning the error. Intercepting tool errors before they're sent to Claude is not currently supported in the Java SDK. The runner catches any exception thrown from a tool's `get()` method and converts it into a tool result with `is_error: true` automatically. To control the error content, catch the exception inside your tool and return a custom string. The PHP tool runner does not currently expose tool results before they are appended. Exceptions thrown from a tool's `run` closure are caught and sent to Claude as tool results with `is_error: true` automatically. To inspect or replace error content, use the manual `pushMessages()` pattern shown in [Modifying tool results](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-runner#modifying-tool-results). ```ruby client = Anthropic::Client.new # ... runner = client.beta.messages.tool_runner( model: "claude-opus-5", max_tokens: 1024, tools: [MyTool.new], messages: [{role: "user", content: "Run my_tool with the query 'hello'."}] ) loop do message = runner.next_message break unless message # By the time next_message returns, the runner has run this turn's tools and # appended their results as the last (user-role) message. Inspect them here, # before the next request sends them to Claude. tool_results = runner.params[:messages].last if tool_results && tool_results[:role] == :user && tool_results[:content].is_a?(Array) tool_results[:content].each do |block| if block[:type] == :tool_result && block[:is_error] # Option 1: Raise an exception to stop the loop raise "Tool failed: #{block[:content]}" # Option 2: Log and continue (let Claude handle it) # logger.error("Tool error: #{block[:content]}") end end end puts message.content break if message.stop_reason != :tool_use end ``` ### Modifying tool results You can modify tool results before they're sent back to Claude. This is useful for adding metadata such as `cache_control` to enable [prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) on tool results, or for transforming the tool output. In the Python and TypeScript SDKs, use the tool response method to get the tool result, then modify it before the runner proceeds. Whether you explicitly append the modified result or mutate it in place depends on the SDK. See the code comments in each tab. ```python client = anthropic.Anthropic() # ... runner = client.beta.messages.tool_runner( model="claude-opus-5", max_tokens=1024, tools=[search_documents], messages=[ { "role": "user", "content": "Search for information about the climate of San Francisco", } ], ) for message in runner: tool_response = runner.generate_tool_call_response() if tool_response is not None: # tool_response is a dict: {"role": "user", "content": [...]} # Modify the tool result to add cache control for block in tool_response["content"]: if block["type"] == "tool_result": # Add cache_control to cache this tool result block["cache_control"] = {"type": "ephemeral"} # Append the modified response (this prevents auto-append of the original) runner.append_messages(message, tool_response) print(message.content) ``` ```typescript const client = new Anthropic(); // ... const runner = client.beta.messages.toolRunner({ model: "claude-opus-5", max_tokens: 1024, tools: [searchDocuments], messages: [ { role: "user", content: "Search for information about the climate of San Francisco" } ] }); for await (const message of runner) { const toolResultMessage = await runner.generateToolResponse(); if (toolResultMessage && typeof toolResultMessage.content !== "string") { // Modify the tool result to add cache control for (const block of toolResultMessage.content) { if (block.type === "tool_result") { // Add cache_control to cache this tool result block.cache_control = { type: "ephemeral" }; } } // No pushMessages call needed: the runner auto-appends both the assistant // message and the (now-mutated) cached tool response. } console.log(message.content); } ``` Modifying tool results before they're appended (for example, to add `cache_control`) is not currently supported in the C# SDK. The runner constructs the `tool_result` block internally and provides no hook to alter it. The Go runner does not expose a hook to modify the outer `tool_result` block. You can, however, set `cache_control` on the inner content blocks your handler returns. ```go client := anthropic.NewClient() ctx := context.Background() searchDocuments, err := toolrunner.NewBetaToolFromJSONSchema( "search_documents", "Search documents for relevant information.", func(ctx context.Context, input SearchDocumentsInput) (anthropic.BetaToolResultBlockParamContentUnion, error) { return anthropic.BetaToolResultBlockParamContentUnion{ OfText: &anthropic.BetaTextBlockParam{ Text: fmt.Sprintf("Found 3 documents matching: %s", input.Query), // Set cache_control on the inner content block. The outer // tool_result block's cache_control is not currently // settable through the Go runner. CacheControl: anthropic.NewBetaCacheControlEphemeralParam(), }, }, nil }, ) if err != nil { log.Fatal(err) } runner := client.Beta.Messages.NewToolRunner( []anthropic.BetaTool{searchDocuments}, anthropic.BetaToolRunnerParams{ BetaMessageNewParams: anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock( "Search for information about the climate of San Francisco", )), }, }, }, ) finalMessage, err := runner.RunToCompletion(ctx) if err != nil { log.Fatal(err) } fmt.Println(finalMessage) ``` To set `cache_control` on a tool result, return `BetaToolResultBlockParam.Content` from the tool instead of `String` and set `cacheControl` on the inner text block. The runner does not currently support setting `cache_control` on the outer `tool_result` block. ```java @JsonClassDescription("Look up reference documentation for a topic") static class SearchDocuments implements Supplier { @JsonPropertyDescription("The search query") public String query; @Override public BetaToolResultBlockParam.Content get() { String largeResult = "..."; // a long document worth caching return BetaToolResultBlockParam.Content.ofBlocks(List.of( BetaToolResultBlockParam.Content.Block.ofText( BetaTextBlockParam.builder() .text(largeResult) .cacheControl(BetaCacheControlEphemeral.builder().build()) .build()))); } } ``` The PHP tool runner has no callback to mutate the auto-generated `tool_result` block. To add fields such as `cache_control`, build the tool result yourself and push it. Calling `pushMessages()` skips the runner's auto-append for that turn. ```php $client = new Client(); // ... $runner = $client->beta->messages->toolRunner( maxTokens: 1024, messages: [ ['role' => 'user', 'content' => 'Search for information about the climate of San Francisco.'], ], model: Model::CLAUDE_OPUS_5, tools: [$searchDocuments], ); foreach ($runner as $message) { $toolResults = []; foreach ($message->content as $block) { if ($block instanceof BetaToolUseBlock) { $toolResults[] = [ 'type' => 'tool_result', 'tool_use_id' => $block->id, 'content' => $searchDocuments->run($block->input), // Add cache_control to cache this tool result 'cache_control' => ['type' => 'ephemeral'], ]; } } if ($toolResults !== []) { // pushMessages() flags state as mutated, so the runner skips its // automatic append. Push the assistant message and tool results. $runner->pushMessages( ['role' => 'assistant', 'content' => $message->content], ['role' => 'user', 'content' => $toolResults], ); } // No tool call: leave state untouched so the loop exits. } ``` ```ruby client = Anthropic::Client.new # ... runner = client.beta.messages.tool_runner( model: "claude-opus-5", max_tokens: 1024, tools: [SearchDocuments.new], messages: [{role: "user", content: "Search for information about the climate of San Francisco"}] ) loop do message = runner.next_message break unless message # Access the most recent tool results from the messages array # The runner automatically adds tool results, but you can modify them tool_results_message = runner.params[:messages].last if tool_results_message && tool_results_message[:role] == :user && tool_results_message[:content].is_a?(Array) tool_results_message[:content].each do |block| if block[:type] == :tool_result # Modify the tool result to add cache control block[:cache_control] = {type: "ephemeral"} end end end puts message.content break if message.stop_reason != :tool_use end ``` Adding `cache_control` to tool results is particularly useful when tools return large amounts of data (such as document search results) that you want to cache for subsequent API calls. See [Prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) for more details on caching strategies. ## Streaming Enable streaming to process each turn's response incrementally. Each iteration yields a stream object that you can iterate for events. Set `stream=True` and use `get_final_message()` to get the accumulated message. ```python client = anthropic.Anthropic() # ... runner = client.beta.messages.tool_runner( model="claude-opus-5", max_tokens=1024, tools=[calculate_sum], messages=[{"role": "user", "content": "What is 15 + 27?"}], stream=True, ) # When streaming, the runner returns BetaMessageStream for message_stream in runner: for event in message_stream: print("event:", event) print("message:", message_stream.get_final_message()) print(runner.until_done()) ``` Set `stream: true` and use `finalMessage()` to get the accumulated message. ```typescript const client = new Anthropic(); // ... const runner = client.beta.messages.toolRunner({ model: "claude-opus-5", max_tokens: 1024, messages: [{ role: "user", content: "What is the weather in San Francisco?" }], tools: [getWeatherTool], stream: true }); // When streaming, the runner returns BetaMessageStream for await (const messageStream of runner) { for await (const event of messageStream) { console.log("event:", event); } console.log("message:", await messageStream.finalMessage()); } console.log(await runner); ``` Call `runner.Streaming()` to get a nested async sequence: one inner stream for each API call. ```csharp var client = new AnthropicClient(); // ... var runner = client.Beta.Messages.ToolRunner( new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [ new() { Role = Role.User, Content = "What is 15 + 27?" }, ], }, [calculateSumTool] ); await foreach (var stream in runner.Streaming()) { await foreach (var streamEvent in stream) { if ( streamEvent.TryPickContentBlockDelta(out var deltaEvent) && deltaEvent.Delta.TryPickText(out var textDelta) ) { Console.Write(textDelta.Text); } } Console.WriteLine(); } ``` Use `NewToolRunnerStreaming` and iterate `runner.AllStreaming(ctx)`. Each outer iteration yields a stream of events for one API call. ```go client := anthropic.NewClient() ctx := context.Background() // ... runner := client.Beta.Messages.NewToolRunnerStreaming( []anthropic.BetaTool{calculateSum}, anthropic.BetaToolRunnerParams{ BetaMessageNewParams: anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("What is 15 + 27?")), }, }, }, ) for events, err := range runner.AllStreaming(ctx) { if err != nil { log.Fatal(err) } for event, err := range events { if err != nil { log.Fatal(err) } switch eventVariant := event.AsAny().(type) { case anthropic.BetaRawContentBlockDeltaEvent: switch deltaVariant := eventVariant.Delta.AsAny().(type) { case anthropic.BetaTextDelta: fmt.Print(deltaVariant.Text) case anthropic.BetaInputJSONDelta: fmt.Print(deltaVariant.PartialJSON) } case anthropic.BetaRawMessageStopEvent: fmt.Println() } } } ``` Call `runner.streaming()` to get a stream for each turn. Each `StreamResponse` must be closed after use. ```java void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); BetaToolRunner runner = client.beta() .messages() .toolRunner(MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .addBeta("structured-outputs-2025-11-13") .addUserMessage("What is 15 + 27?") .addTool(CalculateSum.class) .build()); for (StreamResponse stream : runner.streaming()) { try (stream) { stream.stream().forEach(event -> IO.println("event: " + event)); } } } ``` Streaming is not currently available with the PHP tool runner. Use `each_streaming` to iterate over streaming events. ```ruby client = Anthropic::Client.new # ... runner = client.beta.messages.tool_runner( model: "claude-opus-5", max_tokens: 1024, tools: [CalculateSum.new], messages: [{role: "user", content: "What is 15 + 27?"}] ) runner.each_streaming do |stream| stream.each do |event| case event when Anthropic::Streaming::TextEvent print event.text when Anthropic::Streaming::InputJsonEvent print event.partial_json end end puts end ``` ## Next steps Enforce JSON Schema compliance on Claude's tool inputs with grammar-constrained sampling. Parse `tool_use` blocks, format `tool_result` responses, and handle errors with `is_error`. Enable, format, and disable parallel tool calls, with message-history guidance and troubleshooting. Specify tool schemas, write effective descriptions, and control when Claude calls your tools. --- title: Tool search tool url: https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool description: Scale to hundreds or thousands of tools by letting Claude search your tool catalog and load only the tools it needs. --- The tool search tool lets Claude work with hundreds or thousands of tools by discovering and loading them on demand. Instead of loading all tool definitions into the context window up front, Claude searches your tool catalog (including tool names, descriptions, argument names, and argument descriptions) and loads only the tools it needs. Loading every tool definition up front causes two problems as a tool library grows: * **Context bloat:** A typical multiserver setup (GitHub, Slack, Sentry, Grafana, and Splunk) can consume \~55k tokens in definitions before Claude does any work. Tool search typically reduces this by over 85 percent, loading only the 3–5 tools Claude needs for a given request. * **Tool selection accuracy:** Claude's ability to pick the right tool degrades once you exceed 30–50 available tools. Because tool search loads only a focused set of relevant tools on demand, selection accuracy stays high even across thousands of tools. Tool search is generally available on the Claude API. For supported models, see [Model compatibility](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool#model-compatibility). For background on the scaling challenges that tool search solves, see [Advanced tool use](https://www.anthropic.com/engineering/advanced-tool-use). Tool search's on-demand loading is also an instance of the broader just-in-time retrieval principle described in [Effective context engineering](https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents). Tool search runs as a server-side tool, but you can also implement your own client-side tool search. See [Custom tool search implementation](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool#custom-tool-search-implementation) for details. Share feedback on this feature through the [feedback form](https://forms.gle/MhcGFFwLxuwnWTkYA). For how zero data retention (ZDR) applies to this feature, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention). On Amazon Bedrock, server-side tool search is available only through the [InvokeModel API](https://docs.aws.amazon.com/bedrock/latest/userguide/bedrock-runtime_example_bedrock-runtime_InvokeModel_AnthropicClaude_section.html), not the Converse API. On [Claude Platform on AWS](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws), server-side tool search works identically to the Claude API. Claude Platform on AWS uses the Anthropic Messages API directly, so there is no InvokeModel or Converse distinction. ## Model compatibility Both tool search variants are available on the following models: | Model | Tool versions | | ---------------------------------------------- | ------------------------------------------------------------------- | | Claude Fable 5 (claude-fable-5) | `tool_search_tool_regex_20251119`, `tool_search_tool_bm25_20251119` | | Claude Mythos 5 (claude-mythos-5) | `tool_search_tool_regex_20251119`, `tool_search_tool_bm25_20251119` | | Claude Opus 5 (claude-opus-5) | `tool_search_tool_regex_20251119`, `tool_search_tool_bm25_20251119` | | Claude Opus 4.8 (claude-opus-4-8) | `tool_search_tool_regex_20251119`, `tool_search_tool_bm25_20251119` | | Claude Opus 4.7 (claude-opus-4-7) | `tool_search_tool_regex_20251119`, `tool_search_tool_bm25_20251119` | | Claude Opus 4.6 (claude-opus-4-6) | `tool_search_tool_regex_20251119`, `tool_search_tool_bm25_20251119` | | Claude Sonnet 4.6 (claude-sonnet-4-6) | `tool_search_tool_regex_20251119`, `tool_search_tool_bm25_20251119` | | Claude Opus 4.5 (claude-opus-4-5-20251101) | `tool_search_tool_regex_20251119`, `tool_search_tool_bm25_20251119` | | Claude Sonnet 4.5 (claude-sonnet-4-5-20250929) | `tool_search_tool_regex_20251119`, `tool_search_tool_bm25_20251119` | | Claude Haiku 4.5 (claude-haiku-4-5-20251001) | `tool_search_tool_regex_20251119`, `tool_search_tool_bm25_20251119` | Claude Opus 4.1 and earlier models don't support the tool search tool. ## How tool search works There are two tool search variants: * **Regex** (`tool_search_tool_regex_20251119`): Claude constructs regex patterns to search for tools. * **BM25** (`tool_search_tool_bm25_20251119`): Claude uses natural language queries to search for tools. When you enable the tool search tool: 1. You include a tool search tool (for example, `tool_search_tool_regex_20251119` or `tool_search_tool_bm25_20251119`) in your `tools` list. 2. You provide every tool definition in the `tools` array and set `defer_loading: true` on the tools that shouldn't load up front. At least one tool, normally the tool search tool itself, must stay non-deferred. 3. Initially, Claude's context contains only the tool search tool and any non-deferred tools. 4. When Claude needs additional tools, it searches using a tool search tool. 5. The API runs the search and returns the matching tools as `tool_reference` blocks (up to 5 by default; Claude can set a `limit` in its search input). 6. The API automatically expands these references into full tool definitions. 7. Claude selects from the discovered tools and calls them. ## Quick start The following example includes the tool search tool and two deferred tools: ```bash cURL curl https://api.anthropic.com/v1/messages \ --header "x-api-key: $ANTHROPIC_API_KEY" \ --header "anthropic-version: 2023-06-01" \ --header "content-type: application/json" \ --data '{ "model": "claude-opus-5", "max_tokens": 2048, "messages": [ { "role": "user", "content": "What is the weather in San Francisco?" } ], "tools": [ { "type": "tool_search_tool_regex_20251119", "name": "tool_search_tool_regex" }, { "name": "get_weather", "description": "Get the weather at a specific location", "input_schema": { "type": "object", "properties": { "location": {"type": "string"}, "unit": { "type": "string", "enum": ["celsius", "fahrenheit"] } }, "required": ["location"] }, "defer_loading": true }, { "name": "search_files", "description": "Search through files in the workspace", "input_schema": { "type": "object", "properties": { "query": {"type": "string"}, "file_types": { "type": "array", "items": {"type": "string"} } }, "required": ["query"] }, "defer_loading": true } ] }' ``` ```bash CLI ant messages create <<'YAML' model: claude-opus-5 max_tokens: 2048 messages: - role: user content: What is the weather in San Francisco? tools: - type: tool_search_tool_regex_20251119 name: tool_search_tool_regex - name: get_weather description: Get the weather at a specific location input_schema: type: object properties: location: type: string unit: type: string enum: [celsius, fahrenheit] required: [location] defer_loading: true - name: search_files description: Search through files in the workspace input_schema: type: object properties: query: type: string file_types: type: array items: type: string required: [query] defer_loading: true YAML ``` ```python Python client = anthropic.Anthropic() response = client.messages.create( model="claude-opus-5", max_tokens=2048, messages=[{"role": "user", "content": "What is the weather in San Francisco?"}], tools=[ {"type": "tool_search_tool_regex_20251119", "name": "tool_search_tool_regex"}, { "name": "get_weather", "description": "Get the weather at a specific location", "input_schema": { "type": "object", "properties": { "location": {"type": "string"}, "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}, }, "required": ["location"], }, "defer_loading": True, }, { "name": "search_files", "description": "Search through files in the workspace", "input_schema": { "type": "object", "properties": { "query": {"type": "string"}, "file_types": {"type": "array", "items": {"type": "string"}}, }, "required": ["query"], }, "defer_loading": True, }, ], ) print(response) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 2048, messages: [ { role: "user", content: "What is the weather in San Francisco?" } ], tools: [ { type: "tool_search_tool_regex_20251119", name: "tool_search_tool_regex" }, { name: "get_weather", description: "Get the weather at a specific location", input_schema: { type: "object" as const, properties: { location: { type: "string" }, unit: { type: "string", enum: ["celsius", "fahrenheit"] } }, required: ["location"] }, defer_loading: true }, { name: "search_files", description: "Search through files in the workspace", input_schema: { type: "object" as const, properties: { query: { type: "string" }, file_types: { type: "array", items: { type: "string" } } }, required: ["query"] }, defer_loading: true } ] }); console.log(response); ``` ```csharp C# AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 2048, Messages = [ new() { Role = Role.User, Content = "What is the weather in San Francisco?" } ], Tools = [ new ToolUnion(new ToolSearchToolRegex20251119 { Type = ToolSearchToolRegex20251119Type.ToolSearchToolRegex20251119 }), new ToolUnion(new Tool() { Name = "get_weather", Description = "Get the weather at a specific location", InputSchema = new InputSchema() { Properties = new Dictionary { ["location"] = JsonSerializer.SerializeToElement(new { type = "string" }), ["unit"] = JsonSerializer.SerializeToElement(new { type = "string", @enum = new[] { "celsius", "fahrenheit" } }), }, Required = ["location"], }, DeferLoading = true, }), new ToolUnion(new Tool() { Name = "search_files", Description = "Search through files in the workspace", InputSchema = new InputSchema() { Properties = new Dictionary { ["query"] = JsonSerializer.SerializeToElement(new { type = "string" }), ["file_types"] = JsonSerializer.SerializeToElement(new { type = "array", items = new { type = "string" } }), }, Required = ["query"], }, DeferLoading = true, }), ] }; var message = await client.Messages.Create(parameters); Console.WriteLine(message); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 2048, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("What is the weather in San Francisco?")), }, Tools: []anthropic.ToolUnionParam{ {OfToolSearchToolRegex20251119: &anthropic.ToolSearchToolRegex20251119Param{ Type: anthropic.ToolSearchToolRegex20251119TypeToolSearchToolRegex20251119, }}, {OfTool: &anthropic.ToolParam{ Name: "get_weather", Description: anthropic.String("Get the weather at a specific location"), InputSchema: anthropic.ToolInputSchemaParam{ Properties: map[string]any{ "location": map[string]any{"type": "string"}, "unit": map[string]any{ "type": "string", "enum": []string{"celsius", "fahrenheit"}, }, }, Required: []string{"location"}, }, DeferLoading: anthropic.Bool(true), }}, {OfTool: &anthropic.ToolParam{ Name: "search_files", Description: anthropic.String("Search through files in the workspace"), InputSchema: anthropic.ToolInputSchemaParam{ Properties: map[string]any{ "query": map[string]any{"type": "string"}, "file_types": map[string]any{"type": "array", "items": map[string]any{"type": "string"}}, }, Required: []string{"query"}, }, DeferLoading: anthropic.Bool(true), }}, }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java import com.anthropic.models.messages.ToolSearchToolRegex20251119; void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); InputSchema weatherSchema = InputSchema.builder() .properties(JsonValue.from(Map.of( "location", Map.of("type", "string"), "unit", Map.of( "type", "string", "enum", List.of("celsius", "fahrenheit") ) ))) .putAdditionalProperty("required", JsonValue.from(List.of("location"))) .build(); InputSchema searchSchema = InputSchema.builder() .properties(JsonValue.from(Map.of( "query", Map.of("type", "string"), "file_types", Map.of( "type", "array", "items", Map.of("type", "string") ) ))) .putAdditionalProperty("required", JsonValue.from(List.of("query"))) .build(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(2048L) .addUserMessage("What is the weather in San Francisco?") .addTool(ToolSearchToolRegex20251119.builder() .type(ToolSearchToolRegex20251119.Type.TOOL_SEARCH_TOOL_REGEX_20251119) .build()) .addTool(Tool.builder() .name("get_weather") .description("Get the weather at a specific location") .inputSchema(weatherSchema) .deferLoading(true) .build()) .addTool(Tool.builder() .name("search_files") .description("Search through files in the workspace") .inputSchema(searchSchema) .deferLoading(true) .build()) .build(); Message response = client.messages().create(params); IO.println(response); } ``` ```php PHP $client = new Client(); $message = $client->messages->create( maxTokens: 2048, messages: [ ['role' => 'user', 'content' => 'What is the weather in San Francisco?'], ], model: 'claude-opus-5', tools: [ [ 'type' => 'tool_search_tool_regex_20251119', 'name' => 'tool_search_tool_regex', ], [ 'name' => 'get_weather', 'description' => 'Get the weather at a specific location', 'input_schema' => [ 'type' => 'object', 'properties' => [ 'location' => ['type' => 'string'], 'unit' => [ 'type' => 'string', 'enum' => ['celsius', 'fahrenheit'], ], ], 'required' => ['location'], ], 'defer_loading' => true, ], [ 'name' => 'search_files', 'description' => 'Search through files in the workspace', 'input_schema' => [ 'type' => 'object', 'properties' => [ 'query' => ['type' => 'string'], 'file_types' => [ 'type' => 'array', 'items' => ['type' => 'string'], ], ], 'required' => ['query'], ], 'defer_loading' => true, ], ], ); echo $message; ``` ```ruby Ruby client = Anthropic::Client.new message = client.messages.create( model: "claude-opus-5", max_tokens: 2048, messages: [ { role: "user", content: "What is the weather in San Francisco?" } ], tools: [ { type: "tool_search_tool_regex_20251119", name: "tool_search_tool_regex" }, { name: "get_weather", description: "Get the weather at a specific location", input_schema: { type: "object", properties: { location: { type: "string" }, unit: { type: "string", enum: ["celsius", "fahrenheit"] } }, required: ["location"] }, defer_loading: true }, { name: "search_files", description: "Search through files in the workspace", input_schema: { type: "object", properties: { query: { type: "string" }, file_types: { type: "array", items: { type: "string" } } }, required: ["query"] }, defer_loading: true } ] ) puts message ``` Claude searches the catalog, discovers `get_weather`, and calls it. The response ends with `stop_reason: "tool_use"`. Execute the discovered tool and return a `tool_result` as in [Handle tool calls](https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls). [Response format](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool#response-format) shows the blocks you get back and what to send next. ## Tool definition The tool search tool has two variants: ```json JSON { "type": "tool_search_tool_regex_20251119", "name": "tool_search_tool_regex" } ``` ```json JSON { "type": "tool_search_tool_bm25_20251119", "name": "tool_search_tool_bm25" } ``` **Regex variant query format: Python regex, not natural language** With `tool_search_tool_regex_20251119`, Claude writes Python `re.search()` patterns, not natural language queries. Matching is case-insensitive. Common patterns include the following: * `"weather"`: matches tool names and descriptions containing "weather" * `"get_.*_data"`: matches tools such as `get_user_data` and `get_weather_data` * `"database.*query|query.*database"`: matches either word order Maximum pattern length: 200 characters **BM25 variant query format: natural language** With `tool_search_tool_bm25_20251119`, Claude searches with natural language queries. Maximum query length: 500 characters. ### Deferred tool loading Mark tools for on-demand loading by adding `defer_loading: true`: ```json JSON { "name": "get_weather", "description": "Get current weather for a location", "input_schema": { "type": "object", "properties": { "location": { "type": "string" }, "unit": { "type": "string", "enum": ["celsius", "fahrenheit"] } }, "required": ["location"] }, "defer_loading": true } ``` `defer_loading` controls what enters the context window, not what you send in the request: * You still send every tool's full definition in the `tools` array on every request, including the deferred ones. The API needs them server-side to run the search and expand `tool_reference` blocks. * Tools without `defer_loading` load into context immediately. * Tools with `defer_loading: true` load only when Claude discovers them through search. * Never set `defer_loading: true` on the tool search tool itself. * Keep your 3–5 most frequently used tools non-deferred so Claude can call them without searching first. Both tool search variants (`regex` and `bm25`) search tool names, descriptions, argument names, and argument descriptions. Internally, the API excludes deferred tools from the system-prompt prefix. When Claude discovers a deferred tool through tool search, the API appends a `tool_reference` block inline in the conversation, then expands it into the full tool definition before passing it to Claude. The prefix is untouched, so prompt caching is preserved. The grammar for [strict mode](https://platform.claude.com/docs/en/agents-and-tools/tool-use/strict-tool-use) (the rules that constrain tool-call output to match your schemas) builds from the full toolset, so `defer_loading` and strict mode compose without grammar recompilation. ## Response format When Claude uses the tool search tool, the response includes the following block types: ```json JSON { "role": "assistant", "content": [ { "type": "text", "text": "I'll search for tools to help with the weather information." }, { "type": "server_tool_use", "id": "srvtoolu_01ABC123", "name": "tool_search_tool_regex", "input": { "pattern": "weather", "limit": 10 } }, { "type": "tool_search_tool_result", "tool_use_id": "srvtoolu_01ABC123", "content": { "type": "tool_search_tool_search_result", "tool_references": [{ "type": "tool_reference", "tool_name": "get_weather" }] } }, { "type": "text", "text": "I found a weather tool. Let me get the weather for San Francisco." }, { "type": "tool_use", "id": "toolu_01XYZ789", "name": "get_weather", "input": { "location": "San Francisco", "unit": "fahrenheit" } } ], "stop_reason": "tool_use" } ``` ### Understanding the response * **`server_tool_use`:** Claude's call to the tool search tool. The search runs on Anthropic's servers. Never return a `tool_result` for its `srvtoolu_...` ID. The `input` holds the search (`pattern` for the regex variant, `query` for BM25) and may include an optional `limit`, an integer from 1 to 10,000 that caps how many matching tools the search returns (default: 5). * **`tool_search_tool_result`:** the search results, in a nested `tool_search_tool_search_result` object. Keep it in the message history as is. * **`tool_references`:** an array of `tool_reference` objects pointing to discovered tools. The API expands these for Claude. You never expand them yourself. * **`tool_use`:** Claude's call to a discovered tool. Execute it and return a `tool_result` exactly as in standard tool use. The API automatically expands `tool_reference` blocks into full tool definitions before showing them to Claude. You don't need to handle this expansion yourself, as long as you provide all matching tool definitions in the `tools` parameter. ### Continuing the conversation On the next request, pass the assistant's content back unchanged, including the `server_tool_use` and `tool_search_tool_result` blocks. Add your `tool_result` for the discovered tool in a user message, and send the same `tools` array: the search tool plus every deferred definition. Don't return a `tool_result` for the `srvtoolu_...` ID: the API rejects the request. The API expands `tool_reference` blocks throughout the conversation history, so Claude can reuse discovered tools in later turns without re-searching. A search that matches nothing returns a `tool_search_tool_search_result` with an empty `tool_references` array, not an error. ## MCP integration If your tools come from MCP servers through the [MCP connector](https://platform.claude.com/docs/en/agents-and-tools/mcp-connector), you don't set `defer_loading` on individual tool definitions. Instead, set it once on the `mcp_toolset` entry's `default_config` for the whole server, or per tool in its `configs`. See [MCP toolset configuration](https://platform.claude.com/docs/en/agents-and-tools/mcp-connector#mcp-toolset-configuration). ## Custom tool search implementation You can implement your own tool search logic (for example, using embeddings or semantic search) by returning `tool_reference` blocks from a custom tool. When Claude calls your custom search tool, return a standard `tool_result` with `tool_reference` blocks in the content array: ```json JSON { "type": "tool_result", "tool_use_id": "toolu_your_tool_id", "content": [{ "type": "tool_reference", "tool_name": "discovered_tool_name" }] } ``` Every tool referenced must have a corresponding tool definition in the top-level `tools` parameter, normally with `defer_loading: true`. This lets you use search methods the built-in variants don't provide, such as embedding-based retrieval, and the API expands the returned `tool_reference` blocks the same way. The `tool_search_tool_result` format shown in the [Response format](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool#response-format) section is the server-side format used internally by Anthropic's built-in tool search. For custom client-side implementations, always use the standard `tool_result` format with `tool_reference` content blocks as shown in the preceding example. For a complete example using embeddings, see the [tool search with embeddings](https://platform.claude.com/cookbook/tool-use-tool-search-with-embeddings) recipe. ## Error handling [Tool use examples](https://platform.claude.com/docs/en/agents-and-tools/tool-use/define-tools#providing-tool-use-examples) work with tool search: when Claude discovers a deferred tool, the API expands its `input_examples` along with its definition. ### HTTP errors (400 status) These errors prevent the API from processing the request: **All tools deferred:** ```json { "type": "error", "error": { "type": "invalid_request_error", "message": "At least one tool must have defer_loading=false. All tools cannot be deferred." } } ``` **Missing tool definition:** ```json { "type": "error", "error": { "type": "invalid_request_error", "message": "Tool reference 'unknown_tool' not found in available tools" } } ``` ### Tool result errors (200 status) When a tool search operation fails during execution, the API returns a 200 response with the error in the body: ```json JSON { "type": "tool_search_tool_result", "tool_use_id": "srvtoolu_01ABC123", "content": { "type": "tool_search_tool_result_error", "error_code": "invalid_tool_input", "error_message": "Invalid regular expression pattern: missing ) at position 1" } } ``` The `error_code` field has four possible values: * `invalid_tool_input`: the search input was invalid, for example a malformed regex pattern or a pattern over the 200-character limit * `unavailable`: the search couldn't run, for example because it timed out or the service was unavailable * `too_many_requests`: rate limit exceeded for tool search operations * `execution_time_exceeded`: the search exceeded its execution time limit ### Common mistakes **Cause:** You set `defer_loading: true` on every tool, including the tool search tool. **Fix:** Remove `defer_loading` from the tool search tool: ```json { "type": "tool_search_tool_regex_20251119", "name": "tool_search_tool_regex" } ``` **Cause:** A `tool_reference` points to a tool not in your `tools` array. **Fix:** Ensure every tool that could be discovered has a complete definition: ```json { "name": "my_tool", "description": "Full description here", "input_schema": { "type": "object" }, "defer_loading": true } ``` **Cause:** The regex pattern doesn't match the tool's name, description, argument names, or argument descriptions. **Debugging steps:** 1. Check tool name, description, argument names, and argument descriptions. Claude searches all of these fields. 2. Test your pattern: `import re; re.search(r"your_pattern", "tool_name", re.IGNORECASE)`. 3. Matching is case-insensitive, so casing differences aren't the problem. 4. Claude uses broad patterns such as `".*weather.*"`, not exact matches. **Tip:** Add common keywords to tool descriptions to improve discoverability. ## Prompt caching For how `defer_loading` preserves prompt caching, see [Tool use with prompt caching](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-use-with-prompt-caching). A tool with `defer_loading: true` can't also carry `cache_control`: the API returns a 400. Put the cache breakpoint on a non-deferred tool. ## Streaming With streaming enabled, you'll receive tool search events as part of the stream: ```sse event: content_block_start data: {"type": "content_block_start", "index": 1, "content_block": {"type": "server_tool_use", "id": "srvtoolu_xyz789", "name": "tool_search_tool_regex"}} // Search pattern streamed event: content_block_delta data: {"type": "content_block_delta", "index": 1, "delta": {"type": "input_json_delta", "partial_json": "{\"pattern\":\"weather\"}"}} // Pause while search executes // Search results streamed event: content_block_start data: {"type": "content_block_start", "index": 2, "content_block": {"type": "tool_search_tool_result", "tool_use_id": "srvtoolu_xyz789", "content": {"type": "tool_search_tool_search_result", "tool_references": [{"type": "tool_reference", "tool_name": "get_weather"}]}}} // Claude continues with discovered tools ``` ## Batch requests You can include the tool search tool in the [Messages Batches API](https://platform.claude.com/docs/en/build-with-claude/batch-processing). ## Limits and best practices ### Limits * **Maximum deferred tools:** 10,000 tools with `defer_loading: true` per request * **Search results:** each search returns up to 5 matching tools by default; Claude can set `limit` in its search input to any integer from 1 to 10,000 * **Pattern and query length:** maximum 200 characters for regex patterns and 500 characters for BM25 queries * **Model support:** see [Model compatibility](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool#model-compatibility) ### When to use tool search Use tool search when any of the following apply: * You have 10 or more tools available. * Your tool definitions consume more than 10k tokens. * Tool selection accuracy drops as your toolset grows. * You aggregate multiple MCP servers (200+ tools). * Your tool library grows over time. Standard tool calling, without tool search, is a better fit when you have fewer than 10 tools, every tool is used in every request, or your tool definitions are small (less than 100 tokens total). ### Optimization tips * Keep your 3–5 most frequently used tools non-deferred. * Write clear, descriptive tool names and descriptions. * Use consistent namespacing in tool names: prefix by service or resource (for example, `github_`, `slack_`) so one search matches the whole group. * Use keywords in descriptions that match how users describe tasks. * Add a system prompt section describing available tool categories: "You can search for tools to interact with Slack, GitHub, and Jira." * Monitor which tools Claude discovers to refine your descriptions. ## Usage Tool search isn't metered as a separate server tool. The response's `usage.server_tool_use` object has no tool search field, and the tool definitions that search loads into context count as input tokens like any other tool definition. ## Next steps Let Claude store and retrieve information across conversations by implementing the memory tool's file operations in your application. Directory of Anthropic-provided tools and reference for optional tool definition properties. Configure MCP toolsets with deferred loading. Cache tool definitions across turns and understand what invalidates your cache. Specify tool schemas, write effective descriptions, and control when Claude calls your tools. --- title: Troubleshooting tool use url: https://platform.claude.com/docs/en/agents-and-tools/tool-use/troubleshooting-tool-use description: Fix the most common tool-use errors with symptom-to-fix diagnostic tables. --- Symptom-to-fix tables for the most common tool-use errors. Each fix cross-references the page that owns the feature. ## Claude calls the wrong tool | Symptom | Likely cause | Fix | | ------------------------------------------ | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Claude calls tool A when you wanted tool B | Description ambiguity | Sharpen descriptions. Differentiate tools by WHEN to use them, not only WHAT they do. See [Define tools](https://platform.claude.com/docs/en/agents-and-tools/tool-use/define-tools). | | Claude never calls your tool | Tool name collision or overly-generic schema | Check for duplicate names across your tool list. Add `input_examples` to make the intended use concrete. | | Claude calls with wrong parameter types | Model guessing at ambiguous schema | Add `strict: true` (if your schema is in the supported subset) or add `input_examples`. | ## Claude invents tool parameters | Symptom | Likely cause | Fix | | ------------------------------------------- | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | Parameter that doesn't exist in your schema | Model over-generation without strict mode | Add `strict: true` if your schema is in the [supported subset](https://platform.claude.com/docs/en/agents-and-tools/tool-use/strict-tool-use). | | Parameter values outside your enum | Missing strict mode or too-large enum | Shrink the enum or add `input_examples` showing valid choices. | ## Parallel tool calls don't work | Symptom | Likely cause | Fix | | ------------------------------------------------------------- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Claude calls tools sequentially when parallel would be better | Message history formatting | Send multiple `tool_result` blocks in ONE user message, not one per turn. See [Parallel tool use](https://platform.claude.com/docs/en/agents-and-tools/tool-use/parallel-tool-use). | | `disable_parallel_tool_use` seems ignored | Set too late in the conversation | Must be set on the request that returns `tool_use`. Setting it on a later request has no effect on earlier tool calls. | ## Cache keeps invalidating | Symptom | Likely cause | Fix | | ------------------------------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Every request is a cache miss | `tool_choice`, the thinking configuration, or `output_config.effort` varying between requests | Keep `tool_choice` stable or place the `cache_control` breakpoint before the variation point; hold the thinking configuration and effort level constant for the life of a cached conversation. See [Tool use with prompt caching](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-use-with-prompt-caching) and [Thinking and prompt caching](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-and-prompt-caching). | | Adding a tool mid-conversation breaks cache | Tool prepended to the tools array | Use `defer_loading: true` with tool search to append the tool inline instead of modifying the array head. | ## Errors at request time | Error | Cause | Fix | | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `tool_use ids were found without tool_result blocks immediately after` | Missing `tool_result` for some `tool_use` ids, or `tool_result` is not the first content block in the user message | Return one `tool_result` for every `tool_use` block in the assistant response. Put `tool_result` blocks before any text. See [Handle tool calls](https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls) and [Parallel tool use](https://platform.claude.com/docs/en/agents-and-tools/tool-use/parallel-tool-use). | | `was found without a corresponding _tool_result block` | The previous assistant turn has a `server_tool_use` block with no result block (most often, Claude called it alongside a client tool), and either your next user message ended that turn (for example, with text after the `tool_result` blocks) or the resume request no longer defines that server tool (the message then ends with `but no tool was provided`) | Send a user message containing only the `tool_result` blocks for the client `tool_use` ids and keep the same `tools` array. See [Stop reasons and fallback](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons#tool-use). | | `Input schema is not compatible with strict mode: string patterns are not supported` | Using `pattern` with `strict: true` | Remove the pattern or drop `strict: true`. The `pattern` keyword is not in the supported JSON Schema subset yet. | | `All tools have defer_loading: true` | No tools visible to the model | At least one tool must be immediately loaded. The tool search tool itself must never have `defer_loading: true`. | ## Error: thinking blocks cannot be modified If a request fails with a 400 `invalid_request_error` whose message contains `` `thinking` or `redacted_thinking` blocks in the latest assistant message cannot be modified `` when continuing a conversation after a tool call, your application is altering the assistant's thinking blocks before sending them back. Send the entire assistant message back unchanged, then append your `tool_result`. See [Thinking blocks cannot be modified](https://platform.claude.com/docs/en/api/errors#thinking-blocks-cannot-be-modified) for the full error and fix steps. ## Claude flags tool results as prompt injection | Symptom | Likely cause | Fix | | -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Claude refuses to act on a tool result, or asks the user to confirm instructions that came from it | Your own instructions are being delivered inside the `tool_result` content | Claude is trained to treat instructions inside tool results as potentially untrusted third-party content. Move your instructions out of the tool result: send them in a `user` turn after the `tool_result` block, or, on supported models, in a [mid-conversation system message](https://platform.claude.com/docs/en/build-with-claude/mid-conversation-system-messages). Keep the tool result to just the data. See [Mitigate jailbreaks and prompt injections](https://platform.claude.com/docs/en/test-and-evaluate/strengthen-guardrails/mitigate-jailbreaks#indirect-prompt-injection). | ## JSON escaping differences (Opus 4.6+) | Symptom | Cause | Fix | | -------------------------------------------------------- | ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | | String comparison on tool inputs fails with newer models | Unicode and forward-slash escaping differs between model versions | Parse with `json.loads()` or `JSON.parse()`. Never do raw string matching on serialized input. | ## Next steps Write schemas and descriptions that steer Claude toward the right tool. Execute tools and return results in the required message format. Full directory of Anthropic-schema tools and their version strings. --- title: "Tutorial: Build a tool-using agent" url: https://platform.claude.com/docs/en/agents-and-tools/tool-use/build-a-tool-using-agent description: A guided walkthrough from a single tool call to a production-ready agentic loop. --- This tutorial builds a calendar-management agent in five concentric rings. Each ring is a complete, runnable program that adds exactly one concept to the ring before it. By the end you will have written the agentic loop by hand and then replaced it with the Tool Runner SDK abstraction. The example tool is `create_calendar_event`. Its schema uses nested objects, arrays, and optional fields, so you will see how Claude handles realistic input shapes rather than a single flat string. Every ring runs standalone. Copy any ring into a fresh file and it will run without the code from earlier rings. ## Ring 1: Single tool, single turn The smallest possible tool-using program: one tool, one user message, one tool call, one result. The code is heavily commented so you can map each line to the [tool use lifecycle](https://platform.claude.com/docs/en/agents-and-tools/tool-use/how-tool-use-works). The request sends a `tools` array alongside the user message. When Claude determines that a tool call is needed, the response comes back with `stop_reason: "tool_use"` and a `tool_use` content block containing the tool name, a unique `id`, and the structured `input`. Your code runs the tool, then sends the result back in a `tool_result` block whose `tool_use_id` matches the `id` from the call. ```bash cURL #!/bin/bash # Ring 1: Single tool, single turn. # Define one tool as a JSON fragment. The input_schema is a JSON Schema # object describing the arguments Claude should pass when it calls this # tool. This schema includes nested objects (recurrence), arrays # (attendees), and optional fields, which is closer to real-world tools # than a flat string argument. TOOLS='[ { "name": "create_calendar_event", "description": "Create a calendar event with attendees and optional recurrence.", "input_schema": { "type": "object", "properties": { "title": {"type": "string"}, "start": {"type": "string", "format": "date-time"}, "end": {"type": "string", "format": "date-time"}, "attendees": { "type": "array", "items": {"type": "string", "format": "email"} }, "recurrence": { "type": "object", "properties": { "frequency": {"enum": ["daily", "weekly", "monthly"]}, "count": {"type": "integer", "minimum": 1} } } }, "required": ["title", "start", "end"] } } ]' USER_MSG="Schedule a 30-minute sync with alice@example.com and bob@example.com on Monday, March 30, 2026 at 10am." # Send the user's request along with the tool definition. Claude decides # whether to call the tool based on the request and the tool description. RESPONSE=$(curl -s https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d "$(jq -n \ --argjson tools "$TOOLS" \ --arg msg "$USER_MSG" \ '{ model: "claude-opus-5", max_tokens: 1024, tools: $tools, tool_choice: {type: "auto", disable_parallel_tool_use: true}, messages: [{role: "user", content: $msg}] }')") # When Claude calls a tool, the response has stop_reason "tool_use" # and the content array contains a tool_use block alongside any text. echo "stop_reason: $(echo "$RESPONSE" | jq -r '.stop_reason')" # Find the tool_use block. A response may contain text blocks before the # tool_use block, so filter by type rather than assuming position. TOOL_USE=$(echo "$RESPONSE" | jq '.content[] | select(.type == "tool_use")') TOOL_USE_ID=$(echo "$TOOL_USE" | jq -r '.id') echo "Tool: $(echo "$TOOL_USE" | jq -r '.name')" echo "Input: $(echo "$TOOL_USE" | jq -c '.input')" # Execute the tool. In a real system this would call your calendar API. # Here the result is hardcoded to keep the example self-contained. RESULT='{"event_id": "evt_123", "status": "created"}' # Send the result back. The tool_result block goes in a user message and # its tool_use_id must match the id from the tool_use block above. The # assistant's previous response is included so Claude has the full history. ASSISTANT_CONTENT=$(echo "$RESPONSE" | jq '.content') FOLLOWUP=$(curl -s https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d "$(jq -n \ --argjson tools "$TOOLS" \ --arg msg "$USER_MSG" \ --argjson assistant "$ASSISTANT_CONTENT" \ --arg tool_use_id "$TOOL_USE_ID" \ --arg result "$RESULT" \ '{ model: "claude-opus-5", max_tokens: 1024, tools: $tools, tool_choice: {type: "auto", disable_parallel_tool_use: true}, messages: [ {role: "user", content: $msg}, {role: "assistant", content: $assistant}, {role: "user", content: [ {type: "tool_result", tool_use_id: $tool_use_id, content: $result} ]} ] }')") # With the tool result in hand, Claude produces a final natural-language # answer and stop_reason becomes "end_turn". echo "stop_reason: $(echo "$FOLLOWUP" | jq -r '.stop_reason')" echo "$FOLLOWUP" | jq -r '.content[] | select(.type == "text") | .text' ``` ```bash CLI #!/usr/bin/env bash # Ring 1: Single tool, single turn. # Uses jq for cross-turn message-array state — building an agentic loop in shell # requires JSON manipulation beyond ant's single-call --transform scope. set -euo pipefail USER_MSG="Schedule a 30-minute sync with alice@example.com and bob@example.com on Monday, March 30, 2026 at 10am." MESSAGES=$(jq -n --arg msg "$USER_MSG" '[{role: "user", content: $msg}]') # Define one tool. The input_schema is a JSON Schema object describing # the arguments Claude should pass when it calls this tool. This schema # includes nested objects (recurrence), arrays (attendees), and optional # fields, which is closer to real-world tools than a flat string argument. call_api() { # ant reads the request body as YAML on stdin: no auth headers, no # hand-built JSON envelope. The static keys (model, tools, tool_choice) # live in a quoted heredoc; the growing messages array is appended as # JSON, which YAML accepts as flow syntax. { cat <<'YAML' model: claude-opus-5 max_tokens: 1024 tool_choice: {type: auto, disable_parallel_tool_use: true} tools: - name: create_calendar_event description: Create a calendar event with attendees and optional recurrence. input_schema: type: object properties: title: {type: string} start: {type: string, format: date-time} end: {type: string, format: date-time} attendees: type: array items: {type: string, format: email} recurrence: type: object properties: frequency: {enum: [daily, weekly, monthly]} count: {type: integer, minimum: 1} required: [title, start, end] YAML printf 'messages: %s\n' "$MESSAGES" } | ant messages create --format json } # Send the user's request along with the tool definition. Claude decides # whether to call the tool based on the request and the tool description. RESPONSE=$(call_api) # When Claude calls a tool, the response has stop_reason "tool_use" # and the content array contains a tool_use block alongside any text. echo "stop_reason: $(jq -r '.stop_reason' <<<"$RESPONSE")" # Find the tool_use block. A response may contain text blocks before the # tool_use block, so filter by type rather than assuming position. TOOL_USE=$(jq '.content[] | select(.type == "tool_use")' <<<"$RESPONSE") TOOL_USE_ID=$(jq -r '.id' <<<"$TOOL_USE") echo "Tool: $(jq -r '.name' <<<"$TOOL_USE")" echo "Input: $(jq -c '.input' <<<"$TOOL_USE")" # Execute the tool. In a real system this would call your calendar API. # Here the result is hardcoded to keep the example self-contained. RESULT='{"event_id": "evt_123", "status": "created"}' # Send the result back. The tool_result block goes in a user message and # its tool_use_id must match the id from the tool_use block above. The # assistant's previous response is included so Claude has the full history. MESSAGES=$(jq \ --argjson assistant "$(jq '.content' <<<"$RESPONSE")" \ --arg tool_use_id "$TOOL_USE_ID" \ --arg result "$RESULT" \ '. + [ {role: "assistant", content: $assistant}, {role: "user", content: [ {type: "tool_result", tool_use_id: $tool_use_id, content: $result} ]} ]' <<<"$MESSAGES") FOLLOWUP=$(call_api) # With the tool result in hand, Claude produces a final natural-language # answer and stop_reason becomes "end_turn". echo "stop_reason: $(jq -r '.stop_reason' <<<"$FOLLOWUP")" jq -r '.content[] | select(.type == "text") | .text' <<<"$FOLLOWUP" ``` ```python Python # Ring 1: Single tool, single turn. import json import anthropic # Create a client. It reads ANTHROPIC_API_KEY from the environment. client = anthropic.Anthropic() # Define one tool. The input_schema is a JSON Schema object describing # the arguments Claude should pass when it calls this tool. This schema # includes nested objects (recurrence), arrays (attendees), and optional # fields, which is closer to real-world tools than a flat string argument. tools = [ { "name": "create_calendar_event", "description": "Create a calendar event with attendees and optional recurrence.", "input_schema": { "type": "object", "properties": { "title": {"type": "string"}, "start": {"type": "string", "format": "date-time"}, "end": {"type": "string", "format": "date-time"}, "attendees": { "type": "array", "items": {"type": "string", "format": "email"}, }, "recurrence": { "type": "object", "properties": { "frequency": {"enum": ["daily", "weekly", "monthly"]}, "count": {"type": "integer", "minimum": 1}, }, }, }, "required": ["title", "start", "end"], }, } ] # Send the user's request along with the tool definition. Claude decides # whether to call the tool based on the request and the tool description. response = client.messages.create( model="claude-opus-5", max_tokens=1024, tools=tools, tool_choice={"type": "auto", "disable_parallel_tool_use": True}, messages=[ { "role": "user", "content": "Schedule a 30-minute sync with alice@example.com and bob@example.com on Monday, March 30, 2026 at 10am.", } ], ) # When Claude calls a tool, the response has stop_reason "tool_use" # and the content array contains a tool_use block alongside any text. print(f"stop_reason: {response.stop_reason}") # Find the tool_use block. A response may contain text blocks before the # tool_use block, so scan the content array rather than assuming position. tool_use = next(block for block in response.content if block.type == "tool_use") print(f"Tool: {tool_use.name}") print(f"Input: {tool_use.input}") # Execute the tool. In a real system this would call your calendar API. # Here the result is hardcoded to keep the example self-contained. result = {"event_id": "evt_123", "status": "created"} # Send the result back. The tool_result block goes in a user message and # its tool_use_id must match the id from the tool_use block above. The # assistant's previous response is included so Claude has the full history. followup = client.messages.create( model="claude-opus-5", max_tokens=1024, tools=tools, tool_choice={"type": "auto", "disable_parallel_tool_use": True}, messages=[ { "role": "user", "content": "Schedule a 30-minute sync with alice@example.com and bob@example.com on Monday, March 30, 2026 at 10am.", }, {"role": "assistant", "content": response.content}, { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": tool_use.id, "content": json.dumps(result), } ], }, ], ) # With the tool result in hand, Claude produces a final natural-language # answer and stop_reason becomes "end_turn". print(f"stop_reason: {followup.stop_reason}") final_text = next(block for block in followup.content if block.type == "text") print(final_text.text) ``` ```typescript TypeScript // Ring 1: Single tool, single turn. import Anthropic from "@anthropic-ai/sdk"; // Create a client. It reads ANTHROPIC_API_KEY from the environment. const client = new Anthropic(); // Define one tool. The input_schema is a JSON Schema object describing // the arguments Claude should pass when it calls this tool. This schema // includes nested objects (recurrence), arrays (attendees), and optional // fields, which is closer to real-world tools than a flat string argument. const tools: Anthropic.Tool[] = [ { name: "create_calendar_event", description: "Create a calendar event with attendees and optional recurrence.", input_schema: { type: "object", properties: { title: { type: "string" }, start: { type: "string", format: "date-time" }, end: { type: "string", format: "date-time" }, attendees: { type: "array", items: { type: "string", format: "email" }, }, recurrence: { type: "object", properties: { frequency: { enum: ["daily", "weekly", "monthly"] }, count: { type: "integer", minimum: 1 }, }, }, }, required: ["title", "start", "end"], }, }, ]; // Send the user's request along with the tool definition. Claude decides // whether to call the tool based on the request and the tool description. const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, tools, tool_choice: { type: "auto", disable_parallel_tool_use: true }, messages: [ { role: "user", content: "Schedule a 30-minute sync with alice@example.com and bob@example.com on Monday, March 30, 2026 at 10am.", }, ], }); // When Claude calls a tool, the response has stop_reason "tool_use" // and the content array contains a tool_use block alongside any text. console.log(`stop_reason: ${response.stop_reason}`); // Find the tool_use block. A response may contain text blocks before the // tool_use block, so scan the content array rather than assuming position. const toolUse = response.content.find( (block): block is Anthropic.ToolUseBlock => block.type === "tool_use", )!; console.log(`Tool: ${toolUse.name}`); console.log(`Input: ${JSON.stringify(toolUse.input)}`); // Execute the tool. In a real system this would call your calendar API. // Here the result is hardcoded to keep the example self-contained. const result = { event_id: "evt_123", status: "created" }; // Send the result back. The tool_result block goes in a user message and // its tool_use_id must match the id from the tool_use block above. The // assistant's previous response is included so Claude has the full history. const followup = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, tools, tool_choice: { type: "auto", disable_parallel_tool_use: true }, messages: [ { role: "user", content: "Schedule a 30-minute sync with alice@example.com and bob@example.com on Monday, March 30, 2026 at 10am.", }, { role: "assistant", content: response.content }, { role: "user", content: [ { type: "tool_result", tool_use_id: toolUse.id, content: JSON.stringify(result), }, ], }, ], }); // With the tool result in hand, Claude produces a final natural-language // answer and stop_reason becomes "end_turn". console.log(`stop_reason: ${followup.stop_reason}`); for (const block of followup.content) { if (block.type === "text") { console.log(block.text); } } ``` ```csharp C# // Ring 1: Single tool, single turn. using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Threading.Tasks; using Anthropic; using Anthropic.Models.Messages; // Create a client. It reads ANTHROPIC_API_KEY from the environment. AnthropicClient client = new(); // Define one tool. The input schema is a JSON Schema object describing // the arguments Claude should pass when it calls this tool. This schema // includes nested objects (recurrence), arrays (attendees), and optional // fields, which is closer to real-world tools than a flat string argument. List tools = [ new ToolUnion(new Tool() { Name = "create_calendar_event", Description = "Create a calendar event with attendees and optional recurrence.", InputSchema = new InputSchema() { Properties = new Dictionary { ["title"] = JsonSerializer.SerializeToElement(new { type = "string" }), ["start"] = JsonSerializer.SerializeToElement(new { type = "string", format = "date-time" }), ["end"] = JsonSerializer.SerializeToElement(new { type = "string", format = "date-time" }), ["attendees"] = JsonSerializer.SerializeToElement(new { type = "array", items = new { type = "string", format = "email" }, }), ["recurrence"] = JsonSerializer.SerializeToElement(new { type = "object", properties = new { frequency = new { @enum = new[] { "daily", "weekly", "monthly" } }, count = new { type = "integer", minimum = 1 }, }, }), }, Required = ["title", "start", "end"], }, }), ]; // Ask for at most one tool call per turn so the single-turn flow below // stays predictable. var toolChoice = new ToolChoice(new ToolChoiceAuto { DisableParallelToolUse = true }); const string userPrompt = "Schedule a 30-minute sync with alice@example.com and bob@example.com on Monday, March 30, 2026 at 10am."; // Send the user's request along with the tool definition. Claude decides // whether to call the tool based on the request and the tool description. var response = await client.Messages.Create(new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Tools = tools, ToolChoice = toolChoice, Messages = [new() { Role = Role.User, Content = userPrompt }], }); // When Claude calls a tool, the response has stop_reason "tool_use" // and the content array contains a tool_use block alongside any text. Console.WriteLine($"stop_reason: {response.StopReason?.Raw()}"); // Find the tool_use block. A response may contain text blocks before the // tool_use block, so scan the content array rather than assuming position. ToolUseBlock? toolUse = null; foreach (var block in response.Content) { if (block.TryPickToolUse(out var picked)) { toolUse = picked; break; } } Console.WriteLine($"Tool: {toolUse!.Name}"); Console.WriteLine($"Input: {JsonSerializer.Serialize(toolUse.Input)}"); // Execute the tool. In a real system this would call your calendar API. // Here the result is hardcoded to keep the example self-contained. var result = """{"event_id": "evt_123", "status": "created"}"""; // Send the result back. The tool_result block goes in a user message and // its tool_use_id must match the id from the tool_use block above. The // assistant's previous response is included so Claude has the full history. List toolResults = [ new ContentBlockParam(new ToolResultBlockParam() { ToolUseID = toolUse.ID, Content = result, }), ]; var followup = await client.Messages.Create(new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Tools = tools, ToolChoice = toolChoice, Messages = [ new() { Role = Role.User, Content = userPrompt }, new() { Role = Role.Assistant, Content = response.Content.Select(block => new ContentBlockParam(block.Json)).ToList() }, new() { Role = Role.User, Content = new MessageParamContent(toolResults) }, ], }); // With the tool result in hand, Claude produces a final natural-language // answer and stop_reason becomes "end_turn". Console.WriteLine($"stop_reason: {followup.StopReason?.Raw()}"); foreach (var block in followup.Content) { if (block.TryPickText(out var text)) { Console.WriteLine(text.Text); } } ``` ```go Go // Ring 1: Single tool, single turn. package main import ( "context" "fmt" "log" "github.com/anthropics/anthropic-sdk-go" ) func main() { // Create a client. It reads ANTHROPIC_API_KEY from the environment. client := anthropic.NewClient() ctx := context.Background() // Define one tool. The input schema is a JSON Schema object describing // the arguments Claude should pass when it calls this tool. This schema // includes nested objects (recurrence), arrays (attendees), and optional // fields, which is closer to real-world tools than a flat string argument. tools := []anthropic.ToolUnionParam{ {OfTool: &anthropic.ToolParam{ Name: "create_calendar_event", Description: anthropic.String("Create a calendar event with attendees and optional recurrence."), InputSchema: anthropic.ToolInputSchemaParam{ Properties: map[string]any{ "title": map[string]any{"type": "string"}, "start": map[string]any{"type": "string", "format": "date-time"}, "end": map[string]any{"type": "string", "format": "date-time"}, "attendees": map[string]any{ "type": "array", "items": map[string]any{"type": "string", "format": "email"}, }, "recurrence": map[string]any{ "type": "object", "properties": map[string]any{ "frequency": map[string]any{"enum": []string{"daily", "weekly", "monthly"}}, "count": map[string]any{"type": "integer", "minimum": 1}, }, }, }, Required: []string{"title", "start", "end"}, }, }}, } // Ask for at most one tool call per turn so the single-turn flow below // stays predictable. toolChoice := anthropic.ToolChoiceUnionParam{ OfAuto: &anthropic.ToolChoiceAutoParam{DisableParallelToolUse: anthropic.Bool(true)}, } userMessage := anthropic.NewUserMessage(anthropic.NewTextBlock( "Schedule a 30-minute sync with alice@example.com and bob@example.com on Monday, March 30, 2026 at 10am.", )) // Send the user's request along with the tool definition. Claude decides // whether to call the tool based on the request and the tool description. response, err := client.Messages.New(ctx, anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Tools: tools, ToolChoice: toolChoice, Messages: []anthropic.MessageParam{userMessage}, }) if err != nil { log.Fatal(err) } // When Claude calls a tool, the response has stop_reason "tool_use" // and the content array contains a tool_use block alongside any text. fmt.Printf("stop_reason: %s\n", response.StopReason) // Find the tool_use block. A response may contain text blocks before the // tool_use block, so scan the content array rather than assuming position. var toolUse anthropic.ContentBlockUnion for _, block := range response.Content { if block.Type == "tool_use" { toolUse = block break } } fmt.Printf("Tool: %s\n", toolUse.Name) fmt.Printf("Input: %s\n", string(toolUse.Input)) // Execute the tool. In a real system this would call your calendar API. // Here the result is hardcoded to keep the example self-contained. result := `{"event_id": "evt_123", "status": "created"}` // Send the result back. The tool_result block goes in a user message and // its tool_use_id must match the id from the tool_use block above. The // assistant's previous response is included so Claude has the full history. var assistantContent []anthropic.ContentBlockParamUnion for _, block := range response.Content { assistantContent = append(assistantContent, block.ToParam()) } followup, err := client.Messages.New(ctx, anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Tools: tools, ToolChoice: toolChoice, Messages: []anthropic.MessageParam{ userMessage, anthropic.NewAssistantMessage(assistantContent...), anthropic.NewUserMessage(anthropic.NewToolResultBlock(toolUse.ID, result, false)), }, }) if err != nil { log.Fatal(err) } // With the tool result in hand, Claude produces a final natural-language // answer and stop_reason becomes "end_turn". fmt.Printf("stop_reason: %s\n", followup.StopReason) for _, block := range followup.Content { if block.Type == "text" { fmt.Println(block.Text) } } } ``` ```java Java // Ring 1: Single tool, single turn. import com.anthropic.client.AnthropicClient; import com.anthropic.client.okhttp.AnthropicOkHttpClient; import com.anthropic.core.JsonValue; import com.anthropic.models.messages.ContentBlockParam; import com.anthropic.models.messages.Message; import com.anthropic.models.messages.MessageCreateParams; import com.anthropic.models.messages.Model; import com.anthropic.models.messages.Tool; import com.anthropic.models.messages.Tool.InputSchema; import com.anthropic.models.messages.ToolChoiceAuto; import com.anthropic.models.messages.ToolResultBlockParam; import com.anthropic.models.messages.ToolUseBlock; import java.util.List; import java.util.Map; void main() { // Create a client. It reads ANTHROPIC_API_KEY from the environment. AnthropicClient client = AnthropicOkHttpClient.fromEnv(); // Define one tool. The input schema is a JSON Schema object describing // the arguments Claude should pass when it calls this tool. This schema // includes nested objects (recurrence), arrays (attendees), and optional // fields, which is closer to real-world tools than a flat string argument. Tool calendarTool = Tool.builder() .name("create_calendar_event") .description("Create a calendar event with attendees and optional recurrence.") .inputSchema(InputSchema.builder() .properties(JsonValue.from(Map.of( "title", Map.of("type", "string"), "start", Map.of("type", "string", "format", "date-time"), "end", Map.of("type", "string", "format", "date-time"), "attendees", Map.of( "type", "array", "items", Map.of("type", "string", "format", "email") ), "recurrence", Map.of( "type", "object", "properties", Map.of( "frequency", Map.of("enum", List.of("daily", "weekly", "monthly")), "count", Map.of("type", "integer", "minimum", 1) ) ) ))) .required(List.of("title", "start", "end")) .build()) .build(); // Ask for at most one tool call per turn so the single-turn flow below // stays predictable. ToolChoiceAuto toolChoice = ToolChoiceAuto.builder() .disableParallelToolUse(true) .build(); String userPrompt = "Schedule a 30-minute sync with alice@example.com and bob@example.com on Monday, March 30, 2026 at 10am."; // Send the user's request along with the tool definition. Claude decides // whether to call the tool based on the request and the tool description. Message response = client.messages().create(MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addTool(calendarTool) .toolChoice(toolChoice) .addUserMessage(userPrompt) .build()); // When Claude calls a tool, the response has stop_reason "tool_use" // and the content array contains a tool_use block alongside any text. IO.println("stop_reason: " + response.stopReason().orElse(null)); // Find the tool_use block. A response may contain text blocks before the // tool_use block, so scan the content array rather than assuming position. ToolUseBlock toolUse = response.content().stream() .flatMap(block -> block.toolUse().stream()) .findFirst() .orElseThrow(); IO.println("Tool: " + toolUse.name()); IO.println("Input: " + toolUse._input()); // Execute the tool. In a real system this would call your calendar API. // Here the result is hardcoded to keep the example self-contained. String result = "{\"event_id\": \"evt_123\", \"status\": \"created\"}"; // Send the result back. The tool_result block goes in a user message and // its tool_use_id must match the id from the tool_use block above. The // assistant's previous response is included so Claude has the full history. Message followup = client.messages().create(MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addTool(calendarTool) .toolChoice(toolChoice) .addUserMessage(userPrompt) .addMessage(response) .addUserMessageOfBlockParams(List.of(ContentBlockParam.ofToolResult( ToolResultBlockParam.builder() .toolUseId(toolUse.id()) .content(result) .build()))) .build()); // With the tool result in hand, Claude produces a final natural-language // answer and stop_reason becomes "end_turn". IO.println("stop_reason: " + followup.stopReason().orElse(null)); followup.content().stream() .flatMap(block -> block.text().stream()) .forEach(textBlock -> IO.println(textBlock.text())); } ``` ```php PHP 'create_calendar_event', 'description' => 'Create a calendar event with attendees and optional recurrence.', 'input_schema' => [ 'type' => 'object', 'properties' => [ 'title' => ['type' => 'string'], 'start' => ['type' => 'string', 'format' => 'date-time'], 'end' => ['type' => 'string', 'format' => 'date-time'], 'attendees' => [ 'type' => 'array', 'items' => ['type' => 'string', 'format' => 'email'], ], 'recurrence' => [ 'type' => 'object', 'properties' => [ 'frequency' => ['enum' => ['daily', 'weekly', 'monthly']], 'count' => ['type' => 'integer', 'minimum' => 1], ], ], ], 'required' => ['title', 'start', 'end'], ], ], ]; $userMessage = [ 'role' => 'user', 'content' => 'Schedule a 30-minute sync with alice@example.com and bob@example.com on Monday, March 30, 2026 at 10am.', ]; // Ask for at most one tool call per turn so the single-turn flow below // stays predictable. $toolChoice = ToolChoiceAuto::with(disableParallelToolUse: true); // Send the user's request along with the tool definition. Claude decides // whether to call the tool based on the request and the tool description. $response = $client->messages->create( model: 'claude-opus-5', maxTokens: 1024, tools: $tools, toolChoice: $toolChoice, messages: [$userMessage], ); // When Claude calls a tool, the response has stop_reason "tool_use" // and the content array contains a tool_use block alongside any text. printf("stop_reason: %s\n", $response->stopReason); // Find the tool_use block. A response may contain text blocks before the // tool_use block, so scan the content array rather than assuming position. $toolUse = null; foreach ($response->content as $block) { if ($block->type === 'tool_use') { $toolUse = $block; break; } } printf("Tool: %s\n", $toolUse->name); printf("Input: %s\n", json_encode($toolUse->input)); // Execute the tool. In a real system this would call your calendar API. // Here the result is hardcoded to keep the example self-contained. $result = ['event_id' => 'evt_123', 'status' => 'created']; // Send the result back. The tool_result block goes in a user message and // its tool_use_id must match the id from the tool_use block above. The // assistant's previous response is included so Claude has the full history. $followup = $client->messages->create( model: 'claude-opus-5', maxTokens: 1024, tools: $tools, toolChoice: $toolChoice, messages: [ $userMessage, ['role' => 'assistant', 'content' => $response->content], [ 'role' => 'user', 'content' => [ [ 'type' => 'tool_result', 'tool_use_id' => $toolUse->id, 'content' => json_encode($result), ], ], ], ], ); // With the tool result in hand, Claude produces a final natural-language // answer and stop_reason becomes "end_turn". printf("stop_reason: %s\n", $followup->stopReason); foreach ($followup->content as $block) { if ($block->type === 'text') { echo $block->text, "\n"; } } ``` ```ruby Ruby # Ring 1: Single tool, single turn. require "anthropic" # Create a client. It reads ANTHROPIC_API_KEY from the environment. client = Anthropic::Client.new # Define one tool. The input_schema is a JSON Schema object describing # the arguments Claude should pass when it calls this tool. This schema # includes nested objects (recurrence), arrays (attendees), and optional # fields, which is closer to real-world tools than a flat string argument. tools = [ { name: "create_calendar_event", description: "Create a calendar event with attendees and optional recurrence.", input_schema: { type: "object", properties: { title: {type: "string"}, start: {type: "string", format: "date-time"}, end: {type: "string", format: "date-time"}, attendees: { type: "array", items: {type: "string", format: "email"} }, recurrence: { type: "object", properties: { frequency: {enum: ["daily", "weekly", "monthly"]}, count: {type: "integer", minimum: 1} } } }, required: ["title", "start", "end"] } } ] user_message = { role: "user", content: "Schedule a 30-minute sync with alice@example.com and bob@example.com on Monday, March 30, 2026 at 10am." } # Ask for at most one tool call per turn so the single-turn flow below # stays predictable. tool_choice = {type: "auto", disable_parallel_tool_use: true} # Send the user's request along with the tool definition. Claude decides # whether to call the tool based on the request and the tool description. response = client.messages.create( model: "claude-opus-5", max_tokens: 1024, tools: tools, tool_choice: tool_choice, messages: [user_message] ) # When Claude calls a tool, the response has stop_reason "tool_use" # and the content array contains a tool_use block alongside any text. puts "stop_reason: #{response.stop_reason}" # Find the tool_use block. A response may contain text blocks before the # tool_use block, so scan the content array rather than assuming position. tool_use = response.content.find { |block| block.type == :tool_use } puts "Tool: #{tool_use.name}" puts "Input: #{tool_use.input}" # Execute the tool. In a real system this would call your calendar API. # Here the result is hardcoded to keep the example self-contained. result = {event_id: "evt_123", status: "created"} # Send the result back. The tool_result block goes in a user message and # its tool_use_id must match the id from the tool_use block above. The # assistant's previous response is included so Claude has the full history. followup = client.messages.create( model: "claude-opus-5", max_tokens: 1024, tools: tools, tool_choice: tool_choice, messages: [ user_message, {role: "assistant", content: response.content}, { role: "user", content: [ { type: "tool_result", tool_use_id: tool_use.id, content: JSON.generate(result) } ] } ] ) # With the tool result in hand, Claude produces a final natural-language # answer and stop_reason becomes "end_turn". puts "stop_reason: #{followup.stop_reason}" followup.content.each do |block| puts block.text if block.type == :text end ``` **What to expect** ```text Output wrap stop_reason: tool_use Tool: create_calendar_event Input: {'title': 'Sync', 'start': '2026-03-30T10:00:00', 'end': '2026-03-30T10:30:00', 'attendees': ['alice@example.com', 'bob@example.com']} stop_reason: end_turn I've scheduled your 30-minute sync with Alice and Bob for Monday, March 30 at 10am. ``` The first `stop_reason` is `tool_use` because Claude is waiting for the calendar result. After you send the result, the second `stop_reason` is `end_turn` and the content is natural language for the user. ## Ring 2: The agentic loop Ring 1 assumed Claude would call the tool exactly once. Real tasks often need several calls: Claude might create an event, read the confirmation, then create another. The fix is a `while` loop that keeps running tools and feeding results back until `stop_reason` is no longer `"tool_use"`. The other change is conversation history. Instead of rebuilding the `messages` array from scratch on each request, keep a running list and append to it. Every turn sees the complete prior context. ```bash cURL #!/bin/bash # Ring 2: The agentic loop. TOOLS='[ { "name": "create_calendar_event", "description": "Create a calendar event with attendees and optional recurrence.", "input_schema": { "type": "object", "properties": { "title": {"type": "string"}, "start": {"type": "string", "format": "date-time"}, "end": {"type": "string", "format": "date-time"}, "attendees": {"type": "array", "items": {"type": "string", "format": "email"}}, "recurrence": { "type": "object", "properties": { "frequency": {"enum": ["daily", "weekly", "monthly"]}, "count": {"type": "integer", "minimum": 1} } } }, "required": ["title", "start", "end"] } } ]' run_tool() { local name="$1" local input="$2" if [ "$name" = "create_calendar_event" ]; then local title=$(echo "$input" | jq -r '.title') jq -n --arg title "$title" '{event_id: "evt_123", status: "created", title: $title}' else echo "{\"error\": \"Unknown tool: $name\"}" fi } # Keep the full conversation history in a JSON array so each turn sees prior context. MESSAGES='[{"role": "user", "content": "Schedule a weekly team standup every Monday at 9am for the next 4 weeks. Invite the whole team: alice@example.com, bob@example.com, carol@example.com."}]' call_api() { curl -s https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d "$(jq -n --argjson tools "$TOOLS" --argjson messages "$MESSAGES" \ '{model: "claude-opus-5", max_tokens: 1024, tools: $tools, tool_choice: {type: "auto", disable_parallel_tool_use: true}, messages: $messages}')" } RESPONSE=$(call_api) # Loop until Claude stops asking for tools. Each iteration runs the requested # tool, appends the result to history, and asks Claude to continue. while [ "$(echo "$RESPONSE" | jq -r '.stop_reason')" = "tool_use" ]; do TOOL_USE=$(echo "$RESPONSE" | jq '.content[] | select(.type == "tool_use")') TOOL_NAME=$(echo "$TOOL_USE" | jq -r '.name') TOOL_INPUT=$(echo "$TOOL_USE" | jq -c '.input') TOOL_USE_ID=$(echo "$TOOL_USE" | jq -r '.id') RESULT=$(run_tool "$TOOL_NAME" "$TOOL_INPUT") ASSISTANT_CONTENT=$(echo "$RESPONSE" | jq '.content') MESSAGES=$(echo "$MESSAGES" | jq \ --argjson assistant "$ASSISTANT_CONTENT" \ --arg tool_use_id "$TOOL_USE_ID" \ --arg result "$RESULT" \ '. + [ {role: "assistant", content: $assistant}, {role: "user", content: [{type: "tool_result", tool_use_id: $tool_use_id, content: $result}]} ]') RESPONSE=$(call_api) done echo "$RESPONSE" | jq -r '.content[] | select(.type == "text") | .text' ``` ```bash CLI #!/usr/bin/env bash # Ring 2: The agentic loop. # Uses jq for cross-turn message-array state — building an agentic loop in shell # requires JSON manipulation beyond ant's single-call --transform scope. set -euo pipefail run_tool() { local name="$1" input="$2" if [ "$name" = "create_calendar_event" ]; then jq -n --arg title "$(jq -r '.title' <<<"$input")" \ '{event_id: "evt_123", status: "created", title: $title}' else printf '{"error": "Unknown tool: %s"}' "$name" fi } # Keep the full conversation history in a JSON array so each turn sees # prior context. MESSAGES='[{"role": "user", "content": "Schedule a weekly team standup every Monday at 9am for the next 4 weeks. Invite the whole team: alice@example.com, bob@example.com, carol@example.com."}]' call_api() { # ant reads the request body as YAML on stdin: no auth headers, no # hand-built JSON envelope. The static keys (model, tools, tool_choice) # live in a quoted heredoc; the growing messages array is appended as # JSON, which YAML accepts as flow syntax. { cat <<'YAML' model: claude-opus-5 max_tokens: 1024 tool_choice: {type: auto, disable_parallel_tool_use: true} tools: - name: create_calendar_event description: Create a calendar event with attendees and optional recurrence. input_schema: type: object properties: title: {type: string} start: {type: string, format: date-time} end: {type: string, format: date-time} attendees: type: array items: {type: string, format: email} recurrence: type: object properties: frequency: {enum: [daily, weekly, monthly]} count: {type: integer, minimum: 1} required: [title, start, end] YAML printf 'messages: %s\n' "$MESSAGES" } | ant messages create --format json } RESPONSE=$(call_api) # Loop until Claude stops asking for tools. Each iteration runs the # requested tool, appends the result to history, and asks Claude to # continue. while [ "$(jq -r '.stop_reason' <<<"$RESPONSE")" = "tool_use" ]; do TOOL_USE=$(jq '.content[] | select(.type == "tool_use")' <<<"$RESPONSE") TOOL_NAME=$(jq -r '.name' <<<"$TOOL_USE") TOOL_INPUT=$(jq -c '.input' <<<"$TOOL_USE") TOOL_USE_ID=$(jq -r '.id' <<<"$TOOL_USE") RESULT=$(run_tool "$TOOL_NAME" "$TOOL_INPUT") MESSAGES=$(jq \ --argjson assistant "$(jq '.content' <<<"$RESPONSE")" \ --arg tool_use_id "$TOOL_USE_ID" \ --arg result "$RESULT" \ '. + [ {role: "assistant", content: $assistant}, {role: "user", content: [ {type: "tool_result", tool_use_id: $tool_use_id, content: $result} ]} ]' <<<"$MESSAGES") RESPONSE=$(call_api) done jq -r '.content[] | select(.type == "text") | .text' <<<"$RESPONSE" ``` ```python Python # Ring 2: The agentic loop. import json import anthropic client = anthropic.Anthropic() tools = [ { "name": "create_calendar_event", "description": "Create a calendar event with attendees and optional recurrence.", "input_schema": { "type": "object", "properties": { "title": {"type": "string"}, "start": {"type": "string", "format": "date-time"}, "end": {"type": "string", "format": "date-time"}, "attendees": { "type": "array", "items": {"type": "string", "format": "email"}, }, "recurrence": { "type": "object", "properties": { "frequency": {"enum": ["daily", "weekly", "monthly"]}, "count": {"type": "integer", "minimum": 1}, }, }, }, "required": ["title", "start", "end"], }, } ] def run_tool(name, tool_input): if name == "create_calendar_event": return {"event_id": "evt_123", "status": "created", "title": tool_input["title"]} return {"error": f"Unknown tool: {name}"} # Keep the full conversation history in a list so each turn sees prior context. messages = [ { "role": "user", "content": "Schedule a weekly team standup every Monday at 9am for the next 4 weeks. Invite the whole team: alice@example.com, bob@example.com, carol@example.com.", } ] response = client.messages.create( model="claude-opus-5", max_tokens=1024, tools=tools, tool_choice={"type": "auto", "disable_parallel_tool_use": True}, messages=messages, ) # Loop until Claude stops asking for tools. Each iteration runs the requested # tool, appends the result to history, and asks Claude to continue. while response.stop_reason == "tool_use": tool_use = next(block for block in response.content if block.type == "tool_use") result = run_tool(tool_use.name, tool_use.input) messages.append({"role": "assistant", "content": response.content}) messages.append( { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": tool_use.id, "content": json.dumps(result), } ], } ) response = client.messages.create( model="claude-opus-5", max_tokens=1024, tools=tools, tool_choice={"type": "auto", "disable_parallel_tool_use": True}, messages=messages, ) final_text = next(block for block in response.content if block.type == "text") print(final_text.text) ``` ```typescript TypeScript // Ring 2: The agentic loop. import Anthropic from "@anthropic-ai/sdk"; const client = new Anthropic(); const tools: Anthropic.Tool[] = [ { name: "create_calendar_event", description: "Create a calendar event with attendees and optional recurrence.", input_schema: { type: "object", properties: { title: { type: "string" }, start: { type: "string", format: "date-time" }, end: { type: "string", format: "date-time" }, attendees: { type: "array", items: { type: "string", format: "email" }, }, recurrence: { type: "object", properties: { frequency: { enum: ["daily", "weekly", "monthly"] }, count: { type: "integer", minimum: 1 }, }, }, }, required: ["title", "start", "end"], }, }, ]; function runTool(name: string, input: Record) { if (name === "create_calendar_event") { return { event_id: "evt_123", status: "created", title: input.title }; } return { error: `Unknown tool: ${name}` }; } // Keep the full conversation history so each turn sees prior context. const messages: Anthropic.MessageParam[] = [ { role: "user", content: "Schedule a weekly team standup every Monday at 9am for the next 4 weeks. Invite the whole team: alice@example.com, bob@example.com, carol@example.com.", }, ]; let response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, tools, tool_choice: { type: "auto", disable_parallel_tool_use: true }, messages, }); // Loop until Claude stops asking for tools. Each iteration runs the requested // tool, appends the result to history, and asks Claude to continue. while (response.stop_reason === "tool_use") { const toolUse = response.content.find( (block): block is Anthropic.ToolUseBlock => block.type === "tool_use", )!; const result = runTool(toolUse.name, toolUse.input as Record); messages.push({ role: "assistant", content: response.content }); messages.push({ role: "user", content: [ { type: "tool_result", tool_use_id: toolUse.id, content: JSON.stringify(result), }, ], }); response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, tools, tool_choice: { type: "auto", disable_parallel_tool_use: true }, messages, }); } for (const block of response.content) { if (block.type === "text") { console.log(block.text); } } ``` ```csharp C# // Ring 2: The agentic loop. using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Threading.Tasks; using Anthropic; using Anthropic.Models.Messages; AnthropicClient client = new(); List tools = [ new ToolUnion(new Tool() { Name = "create_calendar_event", Description = "Create a calendar event with attendees and optional recurrence.", InputSchema = new InputSchema() { Properties = new Dictionary { ["title"] = JsonSerializer.SerializeToElement(new { type = "string" }), ["start"] = JsonSerializer.SerializeToElement(new { type = "string", format = "date-time" }), ["end"] = JsonSerializer.SerializeToElement(new { type = "string", format = "date-time" }), ["attendees"] = JsonSerializer.SerializeToElement(new { type = "array", items = new { type = "string", format = "email" }, }), ["recurrence"] = JsonSerializer.SerializeToElement(new { type = "object", properties = new { frequency = new { @enum = new[] { "daily", "weekly", "monthly" } }, count = new { type = "integer", minimum = 1 }, }, }), }, Required = ["title", "start", "end"], }, }), ]; // Run the requested tool and return its result as a string. string RunTool(ToolUseBlock toolUse) { if (toolUse.Name == "create_calendar_event") { var title = toolUse.Input.TryGetValue("title", out var t) ? t.GetString() : ""; return JsonSerializer.Serialize(new { event_id = "evt_123", status = "created", title }); } return JsonSerializer.Serialize(new { error = $"Unknown tool: {toolUse.Name}" }); } var toolChoice = new ToolChoice(new ToolChoiceAuto { DisableParallelToolUse = true }); // Keep the full conversation history in a list so each turn sees prior context. List messages = [ new() { Role = Role.User, Content = "Schedule a weekly team standup every Monday at 9am for the next 4 weeks. Invite the whole team: alice@example.com, bob@example.com, carol@example.com.", }, ]; var response = await client.Messages.Create(new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Tools = tools, ToolChoice = toolChoice, Messages = messages, }); // Loop until Claude stops asking for tools. Each iteration runs the requested // tool, appends the result to history, and asks Claude to continue. while (response.StopReason == StopReason.ToolUse) { ToolUseBlock? toolUse = null; foreach (var block in response.Content) { if (block.TryPickToolUse(out var picked)) { toolUse = picked; break; } } var result = RunTool(toolUse!); messages.Add(new() { Role = Role.Assistant, Content = response.Content.Select(block => new ContentBlockParam(block.Json)).ToList(), }); messages.Add(new() { Role = Role.User, Content = new MessageParamContent( [ new ContentBlockParam(new ToolResultBlockParam() { ToolUseID = toolUse!.ID, Content = result }), ]), }); response = await client.Messages.Create(new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Tools = tools, ToolChoice = toolChoice, Messages = messages, }); } foreach (var block in response.Content) { if (block.TryPickText(out var text)) { Console.WriteLine(text.Text); } } ``` ```go Go // Ring 2: The agentic loop. package main import ( "context" "encoding/json" "fmt" "log" "github.com/anthropics/anthropic-sdk-go" ) func runTool(name string, input map[string]any) string { if name == "create_calendar_event" { title, _ := input["title"].(string) return fmt.Sprintf(`{"event_id": "evt_123", "status": "created", "title": %q}`, title) } return fmt.Sprintf(`{"error": "Unknown tool: %s"}`, name) } func main() { client := anthropic.NewClient() ctx := context.Background() tools := []anthropic.ToolUnionParam{ {OfTool: &anthropic.ToolParam{ Name: "create_calendar_event", Description: anthropic.String("Create a calendar event with attendees and optional recurrence."), InputSchema: anthropic.ToolInputSchemaParam{ Properties: map[string]any{ "title": map[string]any{"type": "string"}, "start": map[string]any{"type": "string", "format": "date-time"}, "end": map[string]any{"type": "string", "format": "date-time"}, "attendees": map[string]any{ "type": "array", "items": map[string]any{"type": "string", "format": "email"}, }, "recurrence": map[string]any{ "type": "object", "properties": map[string]any{ "frequency": map[string]any{"enum": []string{"daily", "weekly", "monthly"}}, "count": map[string]any{"type": "integer", "minimum": 1}, }, }, }, Required: []string{"title", "start", "end"}, }, }}, } toolChoice := anthropic.ToolChoiceUnionParam{ OfAuto: &anthropic.ToolChoiceAutoParam{DisableParallelToolUse: anthropic.Bool(true)}, } // Keep the full conversation history in a slice so each turn sees prior context. messages := []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock( "Schedule a weekly team standup every Monday at 9am for the next 4 weeks. Invite the whole team: alice@example.com, bob@example.com, carol@example.com.", )), } response, err := client.Messages.New(ctx, anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Tools: tools, ToolChoice: toolChoice, Messages: messages, }) if err != nil { log.Fatal(err) } // Loop until Claude stops asking for tools. Each iteration runs the requested // tool, appends the result to history, and asks Claude to continue. for response.StopReason == "tool_use" { var toolUse anthropic.ContentBlockUnion for _, block := range response.Content { if block.Type == "tool_use" { toolUse = block break } } var input map[string]any if err := json.Unmarshal(toolUse.Input, &input); err != nil { log.Fatal(err) } result := runTool(toolUse.Name, input) var assistantContent []anthropic.ContentBlockParamUnion for _, block := range response.Content { assistantContent = append(assistantContent, block.ToParam()) } messages = append(messages, anthropic.NewAssistantMessage(assistantContent...)) messages = append(messages, anthropic.NewUserMessage( anthropic.NewToolResultBlock(toolUse.ID, result, false), )) response, err = client.Messages.New(ctx, anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Tools: tools, ToolChoice: toolChoice, Messages: messages, }) if err != nil { log.Fatal(err) } } for _, block := range response.Content { if block.Type == "text" { fmt.Println(block.Text) } } } ``` ```java Java // Ring 2: The agentic loop. import com.anthropic.client.AnthropicClient; import com.anthropic.client.okhttp.AnthropicOkHttpClient; import com.anthropic.core.JsonValue; import com.anthropic.models.messages.ContentBlockParam; import com.anthropic.models.messages.Message; import com.anthropic.models.messages.MessageCreateParams; import com.anthropic.models.messages.MessageParam; import com.anthropic.models.messages.Model; import com.anthropic.models.messages.StopReason; import com.anthropic.models.messages.Tool; import com.anthropic.models.messages.Tool.InputSchema; import com.anthropic.models.messages.ToolChoiceAuto; import com.anthropic.models.messages.ToolResultBlockParam; import com.anthropic.models.messages.ToolUseBlock; import java.util.ArrayList; import java.util.List; import java.util.Map; String runTool(ToolUseBlock toolUse) { // The raw tool input is a JSON object; read fields out of it as a map. Map input = (Map) toolUse._input().asObject().get(); if (toolUse.name().equals("create_calendar_event")) { String title = input.containsKey("title") ? input.get("title").asStringOrThrow() : ""; return "{\"event_id\": \"evt_123\", \"status\": \"created\", \"title\": \"" + title + "\"}"; } return "{\"error\": \"Unknown tool: " + toolUse.name() + "\"}"; } void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); Tool calendarTool = Tool.builder() .name("create_calendar_event") .description("Create a calendar event with attendees and optional recurrence.") .inputSchema(InputSchema.builder() .properties(JsonValue.from(Map.of( "title", Map.of("type", "string"), "start", Map.of("type", "string", "format", "date-time"), "end", Map.of("type", "string", "format", "date-time"), "attendees", Map.of( "type", "array", "items", Map.of("type", "string", "format", "email") ), "recurrence", Map.of( "type", "object", "properties", Map.of( "frequency", Map.of("enum", List.of("daily", "weekly", "monthly")), "count", Map.of("type", "integer", "minimum", 1) ) ) ))) .required(List.of("title", "start", "end")) .build()) .build(); ToolChoiceAuto toolChoice = ToolChoiceAuto.builder() .disableParallelToolUse(true) .build(); // Keep the full conversation history in a list so each turn sees prior context. List messages = new ArrayList<>(); messages.add(MessageParam.builder() .role(MessageParam.Role.USER) .content("Schedule a weekly team standup every Monday at 9am for the next 4 weeks. Invite the whole team: alice@example.com, bob@example.com, carol@example.com.") .build()); Message response = client.messages().create(MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addTool(calendarTool) .toolChoice(toolChoice) .messages(messages) .build()); // Loop until Claude stops asking for tools. Each iteration runs the requested // tool, appends the result to history, and asks Claude to continue. while (response.stopReason().isPresent() && response.stopReason().get().equals(StopReason.TOOL_USE)) { ToolUseBlock toolUse = response.content().stream() .flatMap(block -> block.toolUse().stream()) .findFirst() .orElseThrow(); String result = runTool(toolUse); messages.add(response.toParam()); messages.add(MessageParam.builder() .role(MessageParam.Role.USER) .contentOfBlockParams(List.of(ContentBlockParam.ofToolResult( ToolResultBlockParam.builder() .toolUseId(toolUse.id()) .content(result) .build()))) .build()); response = client.messages().create(MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addTool(calendarTool) .toolChoice(toolChoice) .messages(messages) .build()); } response.content().stream() .flatMap(block -> block.text().stream()) .forEach(textBlock -> IO.println(textBlock.text())); } ``` ```php PHP 'create_calendar_event', 'description' => 'Create a calendar event with attendees and optional recurrence.', 'input_schema' => [ 'type' => 'object', 'properties' => [ 'title' => ['type' => 'string'], 'start' => ['type' => 'string', 'format' => 'date-time'], 'end' => ['type' => 'string', 'format' => 'date-time'], 'attendees' => [ 'type' => 'array', 'items' => ['type' => 'string', 'format' => 'email'], ], 'recurrence' => [ 'type' => 'object', 'properties' => [ 'frequency' => ['enum' => ['daily', 'weekly', 'monthly']], 'count' => ['type' => 'integer', 'minimum' => 1], ], ], ], 'required' => ['title', 'start', 'end'], ], ], ]; function runTool(string $name, array $input): string { if ($name === 'create_calendar_event') { return json_encode([ 'event_id' => 'evt_123', 'status' => 'created', 'title' => $input['title'], ]); } return json_encode(['error' => "Unknown tool: {$name}"]); } $toolChoice = ToolChoiceAuto::with(disableParallelToolUse: true); // Keep the full conversation history in an array so each turn sees prior context. $messages = [ [ 'role' => 'user', 'content' => 'Schedule a weekly team standup every Monday at 9am for the next 4 weeks. Invite the whole team: alice@example.com, bob@example.com, carol@example.com.', ], ]; $response = $client->messages->create( model: 'claude-opus-5', maxTokens: 1024, tools: $tools, toolChoice: $toolChoice, messages: $messages, ); // Loop until Claude stops asking for tools. Each iteration runs the requested // tool, appends the result to history, and asks Claude to continue. while ($response->stopReason === 'tool_use') { $toolUse = null; foreach ($response->content as $block) { if ($block->type === 'tool_use') { $toolUse = $block; break; } } $result = runTool($toolUse->name, $toolUse->input); $messages[] = ['role' => 'assistant', 'content' => $response->content]; $messages[] = [ 'role' => 'user', 'content' => [ [ 'type' => 'tool_result', 'tool_use_id' => $toolUse->id, 'content' => $result, ], ], ]; $response = $client->messages->create( model: 'claude-opus-5', maxTokens: 1024, tools: $tools, toolChoice: $toolChoice, messages: $messages, ); } foreach ($response->content as $block) { if ($block->type === 'text') { echo $block->text, "\n"; } } ``` ```ruby Ruby # Ring 2: The agentic loop. require "anthropic" client = Anthropic::Client.new tools = [ { name: "create_calendar_event", description: "Create a calendar event with attendees and optional recurrence.", input_schema: { type: "object", properties: { title: {type: "string"}, start: {type: "string", format: "date-time"}, end: {type: "string", format: "date-time"}, attendees: { type: "array", items: {type: "string", format: "email"} }, recurrence: { type: "object", properties: { frequency: {enum: ["daily", "weekly", "monthly"]}, count: {type: "integer", minimum: 1} } } }, required: ["title", "start", "end"] } } ] def run_tool(name, input) case name when "create_calendar_event" JSON.generate({event_id: "evt_123", status: "created", title: input[:title]}) else JSON.generate({error: "Unknown tool: #{name}"}) end end tool_choice = {type: "auto", disable_parallel_tool_use: true} # Keep the full conversation history in an array so each turn sees prior context. messages = [ { role: "user", content: "Schedule a weekly team standup every Monday at 9am for the next 4 weeks. Invite the whole team: alice@example.com, bob@example.com, carol@example.com." } ] response = client.messages.create( model: "claude-opus-5", max_tokens: 1024, tools: tools, tool_choice: tool_choice, messages: messages ) # Loop until Claude stops asking for tools. Each iteration runs the requested # tool, appends the result to history, and asks Claude to continue. while response.stop_reason == :tool_use tool_use = response.content.find { |block| block.type == :tool_use } result = run_tool(tool_use.name, tool_use.input) messages << {role: "assistant", content: response.content} messages << { role: "user", content: [ { type: "tool_result", tool_use_id: tool_use.id, content: result } ] } response = client.messages.create( model: "claude-opus-5", max_tokens: 1024, tools: tools, tool_choice: tool_choice, messages: messages ) end response.content.each do |block| puts block.text if block.type == :text end ``` **What to expect** ```text Output wrap I've set up your weekly team standup for the next 4 Mondays at 9am with Alice, Bob, and Carol invited. ``` The loop might run once or several times depending on how Claude breaks down the task. Your code no longer needs to know in advance. ## Ring 3: Multiple tools, parallel calls Agents rarely have just one capability. Add a second tool, `list_calendar_events`, so Claude can check the existing schedule before creating something new. When Claude has multiple independent tool calls to make, it might return several `tool_use` blocks in a single response. Your loop needs to process all of them and send back all results together in one user message. Iterate over every `tool_use` block in `response.content`, not just the first. ```bash cURL #!/bin/bash # Ring 3: Multiple tools, parallel calls. TOOLS='[ { "name": "create_calendar_event", "description": "Create a calendar event with attendees and optional recurrence.", "input_schema": { "type": "object", "properties": { "title": {"type": "string"}, "start": {"type": "string", "format": "date-time"}, "end": {"type": "string", "format": "date-time"}, "attendees": {"type": "array", "items": {"type": "string", "format": "email"}}, "recurrence": { "type": "object", "properties": { "frequency": {"enum": ["daily", "weekly", "monthly"]}, "count": {"type": "integer", "minimum": 1} } } }, "required": ["title", "start", "end"] } }, { "name": "list_calendar_events", "description": "List all calendar events on a given date.", "input_schema": { "type": "object", "properties": {"date": {"type": "string", "format": "date"}}, "required": ["date"] } } ]' run_tool() { case "$1" in create_calendar_event) jq -n --arg title "$(echo "$2" | jq -r '.title')" '{event_id: "evt_123", status: "created", title: $title}' ;; list_calendar_events) echo '{"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}' ;; *) echo "{\"error\": \"Unknown tool: $1\"}" ;; esac } MESSAGES='[{"role": "user", "content": "Check what I have next Monday, then schedule a planning session that avoids any conflicts."}]' call_api() { curl -s https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d "$(jq -n --argjson tools "$TOOLS" --argjson messages "$MESSAGES" \ '{model: "claude-opus-5", max_tokens: 1024, tools: $tools, messages: $messages}')" } RESPONSE=$(call_api) while [ "$(echo "$RESPONSE" | jq -r '.stop_reason')" = "tool_use" ]; do # A single response can contain multiple tool_use blocks. Process all of # them and return all results together in one user message. TOOL_RESULTS='[]' while read -r block; do NAME=$(echo "$block" | jq -r '.name') INPUT=$(echo "$block" | jq -c '.input') ID=$(echo "$block" | jq -r '.id') RESULT=$(run_tool "$NAME" "$INPUT") TOOL_RESULTS=$(echo "$TOOL_RESULTS" | jq --arg id "$ID" --arg result "$RESULT" \ '. + [{type: "tool_result", tool_use_id: $id, content: $result}]') done < <(echo "$RESPONSE" | jq -c '.content[] | select(.type == "tool_use")') MESSAGES=$(echo "$MESSAGES" | jq \ --argjson assistant "$(echo "$RESPONSE" | jq '.content')" \ --argjson results "$TOOL_RESULTS" \ '. + [{role: "assistant", content: $assistant}, {role: "user", content: $results}]') RESPONSE=$(call_api) done echo "$RESPONSE" | jq -r '.content[] | select(.type == "text") | .text' ``` ```bash CLI #!/usr/bin/env bash # Ring 3: Multiple tools, parallel calls. # Uses jq for cross-turn message-array state — building an agentic loop in shell # requires JSON manipulation beyond ant's single-call --transform scope. set -euo pipefail run_tool() { case "$1" in create_calendar_event) jq -n --arg title "$(jq -r '.title' <<<"$2")" \ '{event_id: "evt_123", status: "created", title: $title}' ;; list_calendar_events) echo '{"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}' ;; *) printf '{"error": "Unknown tool: %s"}' "$1" ;; esac } MESSAGES='[{"role": "user", "content": "Check what I have next Monday, then schedule a planning session that avoids any conflicts."}]' call_api() { # ant reads the request body as YAML on stdin: no auth headers, no # hand-built JSON envelope. The static keys (model, tools) live in a # quoted heredoc; the growing messages array is appended as JSON, # which YAML accepts as flow syntax. { cat <<'YAML' model: claude-opus-5 max_tokens: 1024 tools: - name: create_calendar_event description: Create a calendar event with attendees and optional recurrence. input_schema: type: object properties: title: {type: string} start: {type: string, format: date-time} end: {type: string, format: date-time} attendees: type: array items: {type: string, format: email} recurrence: type: object properties: frequency: {enum: [daily, weekly, monthly]} count: {type: integer, minimum: 1} required: [title, start, end] - name: list_calendar_events description: List all calendar events on a given date. input_schema: type: object properties: date: {type: string, format: date} required: [date] YAML printf 'messages: %s\n' "$MESSAGES" } | ant messages create --format json } RESPONSE=$(call_api) while [ "$(jq -r '.stop_reason' <<<"$RESPONSE")" = "tool_use" ]; do # A single response can contain multiple tool_use blocks. Process all # of them and return all results together in one user message. TOOL_RESULTS='[]' while read -r block; do NAME=$(jq -r '.name' <<<"$block") INPUT=$(jq -c '.input' <<<"$block") ID=$(jq -r '.id' <<<"$block") RESULT=$(run_tool "$NAME" "$INPUT") TOOL_RESULTS=$(jq --arg id "$ID" --arg result "$RESULT" \ '. + [{type: "tool_result", tool_use_id: $id, content: $result}]' \ <<<"$TOOL_RESULTS") done < <(jq -c '.content[] | select(.type == "tool_use")' <<<"$RESPONSE") MESSAGES=$(jq \ --argjson assistant "$(jq '.content' <<<"$RESPONSE")" \ --argjson results "$TOOL_RESULTS" \ '. + [ {role: "assistant", content: $assistant}, {role: "user", content: $results} ]' <<<"$MESSAGES") RESPONSE=$(call_api) done jq -r '.content[] | select(.type == "text") | .text' <<<"$RESPONSE" ``` ```python Python # Ring 3: Multiple tools, parallel calls. import json import anthropic client = anthropic.Anthropic() tools = [ { "name": "create_calendar_event", "description": "Create a calendar event with attendees and optional recurrence.", "input_schema": { "type": "object", "properties": { "title": {"type": "string"}, "start": {"type": "string", "format": "date-time"}, "end": {"type": "string", "format": "date-time"}, "attendees": { "type": "array", "items": {"type": "string", "format": "email"}, }, "recurrence": { "type": "object", "properties": { "frequency": {"enum": ["daily", "weekly", "monthly"]}, "count": {"type": "integer", "minimum": 1}, }, }, }, "required": ["title", "start", "end"], }, }, { "name": "list_calendar_events", "description": "List all calendar events on a given date.", "input_schema": { "type": "object", "properties": { "date": {"type": "string", "format": "date"}, }, "required": ["date"], }, }, ] def run_tool(name, tool_input): if name == "create_calendar_event": return {"event_id": "evt_123", "status": "created", "title": tool_input["title"]} if name == "list_calendar_events": return {"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]} return {"error": f"Unknown tool: {name}"} messages = [ { "role": "user", "content": "Check what I have next Monday, then schedule a planning session that avoids any conflicts.", } ] response = client.messages.create( model="claude-opus-5", max_tokens=1024, tools=tools, messages=messages, ) while response.stop_reason == "tool_use": # A single response can contain multiple tool_use blocks. Process all of # them and return all results together in one user message. tool_results = [] for block in response.content: if block.type == "tool_use": result = run_tool(block.name, block.input) tool_results.append( { "type": "tool_result", "tool_use_id": block.id, "content": json.dumps(result), } ) messages.append({"role": "assistant", "content": response.content}) messages.append({"role": "user", "content": tool_results}) response = client.messages.create( model="claude-opus-5", max_tokens=1024, tools=tools, messages=messages, ) final_text = next(block for block in response.content if block.type == "text") print(final_text.text) ``` ```typescript TypeScript // Ring 3: Multiple tools, parallel calls. import Anthropic from "@anthropic-ai/sdk"; const client = new Anthropic(); const tools: Anthropic.Tool[] = [ { name: "create_calendar_event", description: "Create a calendar event with attendees and optional recurrence.", input_schema: { type: "object", properties: { title: { type: "string" }, start: { type: "string", format: "date-time" }, end: { type: "string", format: "date-time" }, attendees: { type: "array", items: { type: "string", format: "email" }, }, recurrence: { type: "object", properties: { frequency: { enum: ["daily", "weekly", "monthly"] }, count: { type: "integer", minimum: 1 }, }, }, }, required: ["title", "start", "end"], }, }, { name: "list_calendar_events", description: "List all calendar events on a given date.", input_schema: { type: "object", properties: { date: { type: "string", format: "date" }, }, required: ["date"], }, }, ]; function runTool(name: string, input: Record) { if (name === "create_calendar_event") { return { event_id: "evt_123", status: "created", title: input.title }; } if (name === "list_calendar_events") { return { events: [{ title: "Existing meeting", start: "14:00", end: "15:00" }], }; } return { error: `Unknown tool: ${name}` }; } const messages: Anthropic.MessageParam[] = [ { role: "user", content: "Check what I have next Monday, then schedule a planning session that avoids any conflicts.", }, ]; let response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, tools, messages, }); while (response.stop_reason === "tool_use") { // A single response can contain multiple tool_use blocks. Process all of // them and return all results together in one user message. const toolResults: Anthropic.ToolResultBlockParam[] = []; for (const block of response.content) { if (block.type === "tool_use") { const result = runTool(block.name, block.input as Record); toolResults.push({ type: "tool_result", tool_use_id: block.id, content: JSON.stringify(result), }); } } messages.push({ role: "assistant", content: response.content }); messages.push({ role: "user", content: toolResults }); response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, tools, messages, }); } for (const block of response.content) { if (block.type === "text") { console.log(block.text); } } ``` ```csharp C# // Ring 3: Multiple tools, parallel calls. using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Threading.Tasks; using Anthropic; using Anthropic.Models.Messages; AnthropicClient client = new(); List tools = [ new ToolUnion(new Tool() { Name = "create_calendar_event", Description = "Create a calendar event with attendees and optional recurrence.", InputSchema = new InputSchema() { Properties = new Dictionary { ["title"] = JsonSerializer.SerializeToElement(new { type = "string" }), ["start"] = JsonSerializer.SerializeToElement(new { type = "string", format = "date-time" }), ["end"] = JsonSerializer.SerializeToElement(new { type = "string", format = "date-time" }), ["attendees"] = JsonSerializer.SerializeToElement(new { type = "array", items = new { type = "string", format = "email" }, }), ["recurrence"] = JsonSerializer.SerializeToElement(new { type = "object", properties = new { frequency = new { @enum = new[] { "daily", "weekly", "monthly" } }, count = new { type = "integer", minimum = 1 }, }, }), }, Required = ["title", "start", "end"], }, }), new ToolUnion(new Tool() { Name = "list_calendar_events", Description = "List all calendar events on a given date.", InputSchema = new InputSchema() { Properties = new Dictionary { ["date"] = JsonSerializer.SerializeToElement(new { type = "string", format = "date" }), }, Required = ["date"], }, }), ]; string RunTool(ToolUseBlock toolUse) { if (toolUse.Name == "create_calendar_event") { var title = toolUse.Input.TryGetValue("title", out var t) ? t.GetString() : ""; return JsonSerializer.Serialize(new { event_id = "evt_123", status = "created", title }); } if (toolUse.Name == "list_calendar_events") { return """{"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}"""; } return JsonSerializer.Serialize(new { error = $"Unknown tool: {toolUse.Name}" }); } List messages = [ new() { Role = Role.User, Content = "Check what I have next Monday, then schedule a planning session that avoids any conflicts.", }, ]; var response = await client.Messages.Create(new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Tools = tools, Messages = messages, }); while (response.StopReason == StopReason.ToolUse) { // A single response can contain multiple tool_use blocks. Process all of // them and return all results together in one user message. List toolResults = []; foreach (var block in response.Content) { if (block.TryPickToolUse(out var toolUse)) { toolResults.Add(new ContentBlockParam(new ToolResultBlockParam() { ToolUseID = toolUse.ID, Content = RunTool(toolUse), })); } } messages.Add(new() { Role = Role.Assistant, Content = response.Content.Select(block => new ContentBlockParam(block.Json)).ToList(), }); messages.Add(new() { Role = Role.User, Content = new MessageParamContent(toolResults) }); response = await client.Messages.Create(new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Tools = tools, Messages = messages, }); } foreach (var block in response.Content) { if (block.TryPickText(out var text)) { Console.WriteLine(text.Text); } } ``` ```go Go // Ring 3: Multiple tools, parallel calls. package main import ( "context" "encoding/json" "fmt" "log" "github.com/anthropics/anthropic-sdk-go" ) func runTool(name string, input map[string]any) string { if name == "create_calendar_event" { title, _ := input["title"].(string) return fmt.Sprintf(`{"event_id": "evt_123", "status": "created", "title": %q}`, title) } if name == "list_calendar_events" { return `{"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}` } return fmt.Sprintf(`{"error": "Unknown tool: %s"}`, name) } func main() { client := anthropic.NewClient() ctx := context.Background() tools := []anthropic.ToolUnionParam{ {OfTool: &anthropic.ToolParam{ Name: "create_calendar_event", Description: anthropic.String("Create a calendar event with attendees and optional recurrence."), InputSchema: anthropic.ToolInputSchemaParam{ Properties: map[string]any{ "title": map[string]any{"type": "string"}, "start": map[string]any{"type": "string", "format": "date-time"}, "end": map[string]any{"type": "string", "format": "date-time"}, "attendees": map[string]any{ "type": "array", "items": map[string]any{"type": "string", "format": "email"}, }, "recurrence": map[string]any{ "type": "object", "properties": map[string]any{ "frequency": map[string]any{"enum": []string{"daily", "weekly", "monthly"}}, "count": map[string]any{"type": "integer", "minimum": 1}, }, }, }, Required: []string{"title", "start", "end"}, }, }}, {OfTool: &anthropic.ToolParam{ Name: "list_calendar_events", Description: anthropic.String("List all calendar events on a given date."), InputSchema: anthropic.ToolInputSchemaParam{ Properties: map[string]any{ "date": map[string]any{"type": "string", "format": "date"}, }, Required: []string{"date"}, }, }}, } messages := []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock( "Check what I have next Monday, then schedule a planning session that avoids any conflicts.", )), } response, err := client.Messages.New(ctx, anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Tools: tools, Messages: messages, }) if err != nil { log.Fatal(err) } for response.StopReason == "tool_use" { // A single response can contain multiple tool_use blocks. Process all of // them and return all results together in one user message. var toolResults []anthropic.ContentBlockParamUnion for _, block := range response.Content { if block.Type == "tool_use" { var input map[string]any if err := json.Unmarshal(block.Input, &input); err != nil { log.Fatal(err) } result := runTool(block.Name, input) toolResults = append(toolResults, anthropic.NewToolResultBlock(block.ID, result, false)) } } var assistantContent []anthropic.ContentBlockParamUnion for _, block := range response.Content { assistantContent = append(assistantContent, block.ToParam()) } messages = append(messages, anthropic.NewAssistantMessage(assistantContent...)) messages = append(messages, anthropic.NewUserMessage(toolResults...)) response, err = client.Messages.New(ctx, anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Tools: tools, Messages: messages, }) if err != nil { log.Fatal(err) } } for _, block := range response.Content { if block.Type == "text" { fmt.Println(block.Text) } } } ``` ```java Java // Ring 3: Multiple tools, parallel calls. import com.anthropic.client.AnthropicClient; import com.anthropic.client.okhttp.AnthropicOkHttpClient; import com.anthropic.core.JsonValue; import com.anthropic.models.messages.ContentBlock; import com.anthropic.models.messages.ContentBlockParam; import com.anthropic.models.messages.Message; import com.anthropic.models.messages.MessageCreateParams; import com.anthropic.models.messages.MessageParam; import com.anthropic.models.messages.Model; import com.anthropic.models.messages.StopReason; import com.anthropic.models.messages.Tool; import com.anthropic.models.messages.Tool.InputSchema; import com.anthropic.models.messages.ToolResultBlockParam; import com.anthropic.models.messages.ToolUseBlock; import java.util.ArrayList; import java.util.List; import java.util.Map; String runTool(ToolUseBlock toolUse) { // The raw tool input is a JSON object; read fields out of it as a map. Map input = (Map) toolUse._input().asObject().get(); if (toolUse.name().equals("create_calendar_event")) { String title = input.containsKey("title") ? input.get("title").asStringOrThrow() : ""; return "{\"event_id\": \"evt_123\", \"status\": \"created\", \"title\": \"" + title + "\"}"; } if (toolUse.name().equals("list_calendar_events")) { return "{\"events\": [{\"title\": \"Existing meeting\", \"start\": \"14:00\", \"end\": \"15:00\"}]}"; } return "{\"error\": \"Unknown tool: " + toolUse.name() + "\"}"; } void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); Tool calendarTool = Tool.builder() .name("create_calendar_event") .description("Create a calendar event with attendees and optional recurrence.") .inputSchema(InputSchema.builder() .properties(JsonValue.from(Map.of( "title", Map.of("type", "string"), "start", Map.of("type", "string", "format", "date-time"), "end", Map.of("type", "string", "format", "date-time"), "attendees", Map.of( "type", "array", "items", Map.of("type", "string", "format", "email") ), "recurrence", Map.of( "type", "object", "properties", Map.of( "frequency", Map.of("enum", List.of("daily", "weekly", "monthly")), "count", Map.of("type", "integer", "minimum", 1) ) ) ))) .required(List.of("title", "start", "end")) .build()) .build(); Tool listTool = Tool.builder() .name("list_calendar_events") .description("List all calendar events on a given date.") .inputSchema(InputSchema.builder() .properties(JsonValue.from(Map.of( "date", Map.of("type", "string", "format", "date") ))) .required(List.of("date")) .build()) .build(); List messages = new ArrayList<>(); messages.add(MessageParam.builder() .role(MessageParam.Role.USER) .content("Check what I have next Monday, then schedule a planning session that avoids any conflicts.") .build()); Message response = client.messages().create(MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addTool(calendarTool) .addTool(listTool) .messages(messages) .build()); while (response.stopReason().isPresent() && response.stopReason().get().equals(StopReason.TOOL_USE)) { // A single response can contain multiple tool_use blocks. Process all of // them and return all results together in one user message. List toolResults = new ArrayList<>(); for (ContentBlock block : response.content()) { if (block.toolUse().isPresent()) { ToolUseBlock toolUse = block.toolUse().get(); toolResults.add(ContentBlockParam.ofToolResult( ToolResultBlockParam.builder() .toolUseId(toolUse.id()) .content(runTool(toolUse)) .build())); } } messages.add(response.toParam()); messages.add(MessageParam.builder() .role(MessageParam.Role.USER) .contentOfBlockParams(toolResults) .build()); response = client.messages().create(MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addTool(calendarTool) .addTool(listTool) .messages(messages) .build()); } response.content().stream() .flatMap(block -> block.text().stream()) .forEach(textBlock -> IO.println(textBlock.text())); } ``` ```php PHP 'create_calendar_event', 'description' => 'Create a calendar event with attendees and optional recurrence.', 'input_schema' => [ 'type' => 'object', 'properties' => [ 'title' => ['type' => 'string'], 'start' => ['type' => 'string', 'format' => 'date-time'], 'end' => ['type' => 'string', 'format' => 'date-time'], 'attendees' => [ 'type' => 'array', 'items' => ['type' => 'string', 'format' => 'email'], ], 'recurrence' => [ 'type' => 'object', 'properties' => [ 'frequency' => ['enum' => ['daily', 'weekly', 'monthly']], 'count' => ['type' => 'integer', 'minimum' => 1], ], ], ], 'required' => ['title', 'start', 'end'], ], ], [ 'name' => 'list_calendar_events', 'description' => 'List all calendar events on a given date.', 'input_schema' => [ 'type' => 'object', 'properties' => [ 'date' => ['type' => 'string', 'format' => 'date'], ], 'required' => ['date'], ], ], ]; function runTool(string $name, array $input): string { if ($name === 'create_calendar_event') { return json_encode([ 'event_id' => 'evt_123', 'status' => 'created', 'title' => $input['title'], ]); } if ($name === 'list_calendar_events') { return json_encode([ 'events' => [['title' => 'Existing meeting', 'start' => '14:00', 'end' => '15:00']], ]); } return json_encode(['error' => "Unknown tool: {$name}"]); } $messages = [ [ 'role' => 'user', 'content' => 'Check what I have next Monday, then schedule a planning session that avoids any conflicts.', ], ]; $response = $client->messages->create( model: 'claude-opus-5', maxTokens: 1024, tools: $tools, messages: $messages, ); while ($response->stopReason === 'tool_use') { // A single response can contain multiple tool_use blocks. Process all of // them and return all results together in one user message. $toolResults = []; foreach ($response->content as $block) { if ($block->type === 'tool_use') { $toolResults[] = [ 'type' => 'tool_result', 'tool_use_id' => $block->id, 'content' => runTool($block->name, $block->input), ]; } } $messages[] = ['role' => 'assistant', 'content' => $response->content]; $messages[] = ['role' => 'user', 'content' => $toolResults]; $response = $client->messages->create( model: 'claude-opus-5', maxTokens: 1024, tools: $tools, messages: $messages, ); } foreach ($response->content as $block) { if ($block->type === 'text') { echo $block->text, "\n"; } } ``` ```ruby Ruby # Ring 3: Multiple tools, parallel calls. require "anthropic" client = Anthropic::Client.new tools = [ { name: "create_calendar_event", description: "Create a calendar event with attendees and optional recurrence.", input_schema: { type: "object", properties: { title: {type: "string"}, start: {type: "string", format: "date-time"}, end: {type: "string", format: "date-time"}, attendees: { type: "array", items: {type: "string", format: "email"} }, recurrence: { type: "object", properties: { frequency: {enum: ["daily", "weekly", "monthly"]}, count: {type: "integer", minimum: 1} } } }, required: ["title", "start", "end"] } }, { name: "list_calendar_events", description: "List all calendar events on a given date.", input_schema: { type: "object", properties: { date: {type: "string", format: "date"} }, required: ["date"] } } ] def run_tool(name, input) case name when "create_calendar_event" JSON.generate({event_id: "evt_123", status: "created", title: input[:title]}) when "list_calendar_events" JSON.generate({events: [{title: "Existing meeting", start: "14:00", end: "15:00"}]}) else JSON.generate({error: "Unknown tool: #{name}"}) end end messages = [ { role: "user", content: "Check what I have next Monday, then schedule a planning session that avoids any conflicts." } ] response = client.messages.create( model: "claude-opus-5", max_tokens: 1024, tools: tools, messages: messages ) while response.stop_reason == :tool_use # A single response can contain multiple tool_use blocks. Process all of # them and return all results together in one user message. tool_results = response.content.select { |block| block.type == :tool_use }.map do |tool_use| { type: "tool_result", tool_use_id: tool_use.id, content: run_tool(tool_use.name, tool_use.input) } end messages << {role: "assistant", content: response.content} messages << {role: "user", content: tool_results} response = client.messages.create( model: "claude-opus-5", max_tokens: 1024, tools: tools, messages: messages ) end response.content.each do |block| puts block.text if block.type == :text end ``` **What to expect** ```text Output wrap I checked your calendar for next Monday and found an existing meeting from 2pm to 3pm. I've scheduled the planning session for 10am to 11am to avoid the conflict. ``` For more on concurrent execution and ordering guarantees, see [Parallel tool use](https://platform.claude.com/docs/en/agents-and-tools/tool-use/parallel-tool-use). ## Ring 4: Error handling Tools fail. A calendar API might reject an event with too many attendees, or a date might be malformed. When a tool raises an error, send the error message back with `is_error: true` instead of crashing. Claude reads the error and can retry with corrected input, ask the user for clarification, or explain the limitation. ```bash cURL #!/bin/bash # Ring 4: Error handling. TOOLS='[ { "name": "create_calendar_event", "description": "Create a calendar event with attendees and optional recurrence.", "input_schema": { "type": "object", "properties": { "title": {"type": "string"}, "start": {"type": "string", "format": "date-time"}, "end": {"type": "string", "format": "date-time"}, "attendees": {"type": "array", "items": {"type": "string", "format": "email"}}, "recurrence": { "type": "object", "properties": { "frequency": {"enum": ["daily", "weekly", "monthly"]}, "count": {"type": "integer", "minimum": 1} } } }, "required": ["title", "start", "end"] } }, { "name": "list_calendar_events", "description": "List all calendar events on a given date.", "input_schema": { "type": "object", "properties": {"date": {"type": "string", "format": "date"}}, "required": ["date"] } } ]' run_tool() { case "$1" in create_calendar_event) local count=$(echo "$2" | jq '.attendees | length // 0') if [ "$count" -gt 10 ]; then echo "ERROR: Too many attendees (max 10)" return 1 fi jq -n --arg title "$(echo "$2" | jq -r '.title')" '{event_id: "evt_123", status: "created", title: $title}' ;; list_calendar_events) echo '{"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}' ;; *) echo "ERROR: Unknown tool: $1" return 1 ;; esac } EMAILS=$(seq 0 14 | sed 's/.*/user&@example.com/' | paste -sd, -) MESSAGES="[{\"role\": \"user\", \"content\": \"Schedule an all-hands with everyone: $EMAILS\"}]" call_api() { curl -s https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d "$(jq -n --argjson tools "$TOOLS" --argjson messages "$MESSAGES" \ '{model: "claude-opus-5", max_tokens: 1024, tools: $tools, messages: $messages}')" } RESPONSE=$(call_api) while [ "$(echo "$RESPONSE" | jq -r '.stop_reason')" = "tool_use" ]; do TOOL_RESULTS='[]' while read -r block; do NAME=$(echo "$block" | jq -r '.name') INPUT=$(echo "$block" | jq -c '.input') ID=$(echo "$block" | jq -r '.id') if OUTPUT=$(run_tool "$NAME" "$INPUT"); then TOOL_RESULTS=$(echo "$TOOL_RESULTS" | jq --arg id "$ID" --arg result "$OUTPUT" \ '. + [{type: "tool_result", tool_use_id: $id, content: $result}]') else # Signal failure so Claude can retry or ask for clarification. TOOL_RESULTS=$(echo "$TOOL_RESULTS" | jq --arg id "$ID" --arg result "$OUTPUT" \ '. + [{type: "tool_result", tool_use_id: $id, content: $result, is_error: true}]') fi done < <(echo "$RESPONSE" | jq -c '.content[] | select(.type == "tool_use")') MESSAGES=$(echo "$MESSAGES" | jq \ --argjson assistant "$(echo "$RESPONSE" | jq '.content')" \ --argjson results "$TOOL_RESULTS" \ '. + [{role: "assistant", content: $assistant}, {role: "user", content: $results}]') RESPONSE=$(call_api) done echo "$RESPONSE" | jq -r '.content[] | select(.type == "text") | .text' ``` ```bash CLI #!/usr/bin/env bash # Ring 4: Error handling. # Uses jq for cross-turn message-array state — building an agentic loop in shell # requires JSON manipulation beyond ant's single-call --transform scope. set -euo pipefail run_tool() { case "$1" in create_calendar_event) local count count=$(jq '.attendees | length // 0' <<<"$2") if [ "$count" -gt 10 ]; then echo "ERROR: Too many attendees (max 10)" return 1 fi jq -n --arg title "$(jq -r '.title' <<<"$2")" \ '{event_id: "evt_123", status: "created", title: $title}' ;; list_calendar_events) echo '{"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}' ;; *) echo "ERROR: Unknown tool: $1" return 1 ;; esac } EMAILS=$(seq 0 14 | sed 's/.*/user&@example.com/' | paste -sd, -) MESSAGES=$(jq -n --arg msg "Schedule an all-hands with everyone: $EMAILS" \ '[{role: "user", content: $msg}]') call_api() { # ant reads the request body as YAML on stdin: no auth headers, no # hand-built JSON envelope. The static keys (model, tools) live in a # quoted heredoc; the growing messages array is appended as JSON, # which YAML accepts as flow syntax. { cat <<'YAML' model: claude-opus-5 max_tokens: 1024 tools: - name: create_calendar_event description: Create a calendar event with attendees and optional recurrence. input_schema: type: object properties: title: {type: string} start: {type: string, format: date-time} end: {type: string, format: date-time} attendees: type: array items: {type: string, format: email} recurrence: type: object properties: frequency: {enum: [daily, weekly, monthly]} count: {type: integer, minimum: 1} required: [title, start, end] - name: list_calendar_events description: List all calendar events on a given date. input_schema: type: object properties: date: {type: string, format: date} required: [date] YAML printf 'messages: %s\n' "$MESSAGES" } | ant messages create --format json } RESPONSE=$(call_api) while [ "$(jq -r '.stop_reason' <<<"$RESPONSE")" = "tool_use" ]; do TOOL_RESULTS='[]' while read -r block; do NAME=$(jq -r '.name' <<<"$block") INPUT=$(jq -c '.input' <<<"$block") ID=$(jq -r '.id' <<<"$block") if OUTPUT=$(run_tool "$NAME" "$INPUT"); then TOOL_RESULTS=$(jq --arg id "$ID" --arg result "$OUTPUT" \ '. + [{type: "tool_result", tool_use_id: $id, content: $result}]' \ <<<"$TOOL_RESULTS") else # Signal failure so Claude can retry or ask for clarification. TOOL_RESULTS=$(jq --arg id "$ID" --arg result "$OUTPUT" \ '. + [{type: "tool_result", tool_use_id: $id, content: $result, is_error: true}]' \ <<<"$TOOL_RESULTS") fi done < <(jq -c '.content[] | select(.type == "tool_use")' <<<"$RESPONSE") MESSAGES=$(jq \ --argjson assistant "$(jq '.content' <<<"$RESPONSE")" \ --argjson results "$TOOL_RESULTS" \ '. + [ {role: "assistant", content: $assistant}, {role: "user", content: $results} ]' <<<"$MESSAGES") RESPONSE=$(call_api) done jq -r '.content[] | select(.type == "text") | .text' <<<"$RESPONSE" ``` ```python Python # Ring 4: Error handling. import json import anthropic client = anthropic.Anthropic() tools = [ { "name": "create_calendar_event", "description": "Create a calendar event with attendees and optional recurrence.", "input_schema": { "type": "object", "properties": { "title": {"type": "string"}, "start": {"type": "string", "format": "date-time"}, "end": {"type": "string", "format": "date-time"}, "attendees": { "type": "array", "items": {"type": "string", "format": "email"}, }, "recurrence": { "type": "object", "properties": { "frequency": {"enum": ["daily", "weekly", "monthly"]}, "count": {"type": "integer", "minimum": 1}, }, }, }, "required": ["title", "start", "end"], }, }, { "name": "list_calendar_events", "description": "List all calendar events on a given date.", "input_schema": { "type": "object", "properties": { "date": {"type": "string", "format": "date"}, }, "required": ["date"], }, }, ] def run_tool(name, tool_input): if name == "create_calendar_event": if "attendees" in tool_input and len(tool_input["attendees"]) > 10: raise ValueError("Too many attendees (max 10)") return {"event_id": "evt_123", "status": "created", "title": tool_input["title"]} if name == "list_calendar_events": return {"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]} raise ValueError(f"Unknown tool: {name}") messages = [ { "role": "user", "content": "Schedule an all-hands with everyone: " + ", ".join(f"user{i}@example.com" for i in range(15)), } ] response = client.messages.create( model="claude-opus-5", max_tokens=1024, tools=tools, messages=messages, ) while response.stop_reason == "tool_use": tool_results = [] for block in response.content: if block.type == "tool_use": try: result = run_tool(block.name, block.input) tool_results.append( {"type": "tool_result", "tool_use_id": block.id, "content": json.dumps(result)} ) except Exception as exc: # Signal failure so Claude can retry or ask for clarification. tool_results.append( { "type": "tool_result", "tool_use_id": block.id, "content": str(exc), "is_error": True, } ) messages.append({"role": "assistant", "content": response.content}) messages.append({"role": "user", "content": tool_results}) response = client.messages.create( model="claude-opus-5", max_tokens=1024, tools=tools, messages=messages, ) final_text = next(block for block in response.content if block.type == "text") print(final_text.text) ``` ```typescript TypeScript // Ring 4: Error handling. import Anthropic from "@anthropic-ai/sdk"; const client = new Anthropic(); const tools: Anthropic.Tool[] = [ { name: "create_calendar_event", description: "Create a calendar event with attendees and optional recurrence.", input_schema: { type: "object", properties: { title: { type: "string" }, start: { type: "string", format: "date-time" }, end: { type: "string", format: "date-time" }, attendees: { type: "array", items: { type: "string", format: "email" }, }, recurrence: { type: "object", properties: { frequency: { enum: ["daily", "weekly", "monthly"] }, count: { type: "integer", minimum: 1 }, }, }, }, required: ["title", "start", "end"], }, }, { name: "list_calendar_events", description: "List all calendar events on a given date.", input_schema: { type: "object", properties: { date: { type: "string", format: "date" }, }, required: ["date"], }, }, ]; function runTool(name: string, input: Record) { if (name === "create_calendar_event") { const attendees = input.attendees as string[] | undefined; if (attendees && attendees.length > 10) { throw new Error("Too many attendees (max 10)"); } return { event_id: "evt_123", status: "created", title: input.title }; } if (name === "list_calendar_events") { return { events: [{ title: "Existing meeting", start: "14:00", end: "15:00" }], }; } throw new Error(`Unknown tool: ${name}`); } const emails = Array.from({ length: 15 }, (_, i) => `user${i}@example.com`); const messages: Anthropic.MessageParam[] = [ { role: "user", content: `Schedule an all-hands with everyone: ${emails.join(", ")}`, }, ]; let response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, tools, messages, }); while (response.stop_reason === "tool_use") { const toolResults: Anthropic.ToolResultBlockParam[] = []; for (const block of response.content) { if (block.type === "tool_use") { try { const result = runTool(block.name, block.input as Record); toolResults.push({ type: "tool_result", tool_use_id: block.id, content: JSON.stringify(result), }); } catch (err) { // Signal failure so Claude can retry or ask for clarification. toolResults.push({ type: "tool_result", tool_use_id: block.id, content: String(err), is_error: true, }); } } } messages.push({ role: "assistant", content: response.content }); messages.push({ role: "user", content: toolResults }); response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, tools, messages, }); } for (const block of response.content) { if (block.type === "text") { console.log(block.text); } } ``` ```csharp C# // Ring 4: Error handling. using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Threading.Tasks; using Anthropic; using Anthropic.Models.Messages; AnthropicClient client = new(); List tools = [ new ToolUnion(new Tool() { Name = "create_calendar_event", Description = "Create a calendar event with attendees and optional recurrence.", InputSchema = new InputSchema() { Properties = new Dictionary { ["title"] = JsonSerializer.SerializeToElement(new { type = "string" }), ["start"] = JsonSerializer.SerializeToElement(new { type = "string", format = "date-time" }), ["end"] = JsonSerializer.SerializeToElement(new { type = "string", format = "date-time" }), ["attendees"] = JsonSerializer.SerializeToElement(new { type = "array", items = new { type = "string", format = "email" }, }), ["recurrence"] = JsonSerializer.SerializeToElement(new { type = "object", properties = new { frequency = new { @enum = new[] { "daily", "weekly", "monthly" } }, count = new { type = "integer", minimum = 1 }, }, }), }, Required = ["title", "start", "end"], }, }), new ToolUnion(new Tool() { Name = "list_calendar_events", Description = "List all calendar events on a given date.", InputSchema = new InputSchema() { Properties = new Dictionary { ["date"] = JsonSerializer.SerializeToElement(new { type = "string", format = "date" }), }, Required = ["date"], }, }), ]; string RunTool(ToolUseBlock toolUse) { if (toolUse.Name == "create_calendar_event") { if (toolUse.Input.TryGetValue("attendees", out var attendees) && attendees.GetArrayLength() > 10) { throw new InvalidOperationException("Too many attendees (max 10)"); } var title = toolUse.Input.TryGetValue("title", out var t) ? t.GetString() : ""; return JsonSerializer.Serialize(new { event_id = "evt_123", status = "created", title }); } if (toolUse.Name == "list_calendar_events") { return """{"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}"""; } throw new InvalidOperationException($"Unknown tool: {toolUse.Name}"); } // Build a request that exceeds the tool's attendee limit so the error path runs. var emails = string.Join(", ", Enumerable.Range(0, 15).Select(i => $"user{i}@example.com")); List messages = [ new() { Role = Role.User, Content = $"Schedule an all-hands with everyone: {emails}" }, ]; var response = await client.Messages.Create(new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Tools = tools, Messages = messages, }); while (response.StopReason == StopReason.ToolUse) { List toolResults = []; foreach (var block in response.Content) { if (block.TryPickToolUse(out var toolUse)) { ToolResultBlockParam toolResult; try { toolResult = new ToolResultBlockParam() { ToolUseID = toolUse.ID, Content = RunTool(toolUse) }; } catch (Exception e) { // Signal failure so Claude can retry or ask for clarification. toolResult = new ToolResultBlockParam() { ToolUseID = toolUse.ID, Content = e.Message, IsError = true, }; } toolResults.Add(new ContentBlockParam(toolResult)); } } messages.Add(new() { Role = Role.Assistant, Content = response.Content.Select(block => new ContentBlockParam(block.Json)).ToList(), }); messages.Add(new() { Role = Role.User, Content = new MessageParamContent(toolResults) }); response = await client.Messages.Create(new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Tools = tools, Messages = messages, }); } foreach (var block in response.Content) { if (block.TryPickText(out var text)) { Console.WriteLine(text.Text); } } ``` ```go Go // Ring 4: Error handling. package main import ( "context" "encoding/json" "fmt" "log" "strings" "github.com/anthropics/anthropic-sdk-go" ) func runTool(name string, input map[string]any) (string, error) { if name == "create_calendar_event" { if attendees, ok := input["attendees"].([]any); ok && len(attendees) > 10 { return "", fmt.Errorf("too many attendees (max 10)") } title, _ := input["title"].(string) return fmt.Sprintf(`{"event_id": "evt_123", "status": "created", "title": %q}`, title), nil } if name == "list_calendar_events" { return `{"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}`, nil } return "", fmt.Errorf("unknown tool: %s", name) } func main() { client := anthropic.NewClient() ctx := context.Background() tools := []anthropic.ToolUnionParam{ {OfTool: &anthropic.ToolParam{ Name: "create_calendar_event", Description: anthropic.String("Create a calendar event with attendees and optional recurrence."), InputSchema: anthropic.ToolInputSchemaParam{ Properties: map[string]any{ "title": map[string]any{"type": "string"}, "start": map[string]any{"type": "string", "format": "date-time"}, "end": map[string]any{"type": "string", "format": "date-time"}, "attendees": map[string]any{ "type": "array", "items": map[string]any{"type": "string", "format": "email"}, }, "recurrence": map[string]any{ "type": "object", "properties": map[string]any{ "frequency": map[string]any{"enum": []string{"daily", "weekly", "monthly"}}, "count": map[string]any{"type": "integer", "minimum": 1}, }, }, }, Required: []string{"title", "start", "end"}, }, }}, {OfTool: &anthropic.ToolParam{ Name: "list_calendar_events", Description: anthropic.String("List all calendar events on a given date."), InputSchema: anthropic.ToolInputSchemaParam{ Properties: map[string]any{ "date": map[string]any{"type": "string", "format": "date"}, }, Required: []string{"date"}, }, }}, } // Build a request that exceeds the tool's attendee limit so the error path runs. emails := make([]string, 15) for i := range emails { emails[i] = fmt.Sprintf("user%d@example.com", i) } messages := []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock( "Schedule an all-hands with everyone: " + strings.Join(emails, ", "), )), } response, err := client.Messages.New(ctx, anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Tools: tools, Messages: messages, }) if err != nil { log.Fatal(err) } for response.StopReason == "tool_use" { var toolResults []anthropic.ContentBlockParamUnion for _, block := range response.Content { if block.Type == "tool_use" { var input map[string]any if err := json.Unmarshal(block.Input, &input); err != nil { log.Fatal(err) } result, toolErr := runTool(block.Name, input) if toolErr != nil { // Signal failure so Claude can retry or ask for clarification. toolResults = append(toolResults, anthropic.NewToolResultBlock(block.ID, toolErr.Error(), true)) } else { toolResults = append(toolResults, anthropic.NewToolResultBlock(block.ID, result, false)) } } } var assistantContent []anthropic.ContentBlockParamUnion for _, block := range response.Content { assistantContent = append(assistantContent, block.ToParam()) } messages = append(messages, anthropic.NewAssistantMessage(assistantContent...)) messages = append(messages, anthropic.NewUserMessage(toolResults...)) response, err = client.Messages.New(ctx, anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Tools: tools, Messages: messages, }) if err != nil { log.Fatal(err) } } for _, block := range response.Content { if block.Type == "text" { fmt.Println(block.Text) } } } ``` ```java Java // Ring 4: Error handling. import com.anthropic.client.AnthropicClient; import com.anthropic.client.okhttp.AnthropicOkHttpClient; import com.anthropic.core.JsonValue; import com.anthropic.models.messages.ContentBlock; import com.anthropic.models.messages.ContentBlockParam; import com.anthropic.models.messages.Message; import com.anthropic.models.messages.MessageCreateParams; import com.anthropic.models.messages.MessageParam; import com.anthropic.models.messages.Model; import com.anthropic.models.messages.StopReason; import com.anthropic.models.messages.Tool; import com.anthropic.models.messages.Tool.InputSchema; import com.anthropic.models.messages.ToolResultBlockParam; import com.anthropic.models.messages.ToolUseBlock; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import java.util.stream.IntStream; String runTool(ToolUseBlock toolUse) { // The raw tool input is a JSON object; read fields out of it as a map. Map input = (Map) toolUse._input().asObject().get(); if (toolUse.name().equals("create_calendar_event")) { int attendeeCount = input.containsKey("attendees") ? ((List) input.get("attendees").asArray().get()).size() : 0; if (attendeeCount > 10) { throw new IllegalArgumentException("Too many attendees (max 10)"); } String title = input.containsKey("title") ? input.get("title").asStringOrThrow() : ""; return "{\"event_id\": \"evt_123\", \"status\": \"created\", \"title\": \"" + title + "\"}"; } if (toolUse.name().equals("list_calendar_events")) { return "{\"events\": [{\"title\": \"Existing meeting\", \"start\": \"14:00\", \"end\": \"15:00\"}]}"; } throw new IllegalArgumentException("Unknown tool: " + toolUse.name()); } void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); Tool calendarTool = Tool.builder() .name("create_calendar_event") .description("Create a calendar event with attendees and optional recurrence.") .inputSchema(InputSchema.builder() .properties(JsonValue.from(Map.of( "title", Map.of("type", "string"), "start", Map.of("type", "string", "format", "date-time"), "end", Map.of("type", "string", "format", "date-time"), "attendees", Map.of( "type", "array", "items", Map.of("type", "string", "format", "email") ), "recurrence", Map.of( "type", "object", "properties", Map.of( "frequency", Map.of("enum", List.of("daily", "weekly", "monthly")), "count", Map.of("type", "integer", "minimum", 1) ) ) ))) .required(List.of("title", "start", "end")) .build()) .build(); Tool listTool = Tool.builder() .name("list_calendar_events") .description("List all calendar events on a given date.") .inputSchema(InputSchema.builder() .properties(JsonValue.from(Map.of( "date", Map.of("type", "string", "format", "date") ))) .required(List.of("date")) .build()) .build(); // Build a request that exceeds the tool's attendee limit so the error path runs. String emails = IntStream.range(0, 15) .mapToObj(i -> "user" + i + "@example.com") .collect(Collectors.joining(", ")); List messages = new ArrayList<>(); messages.add(MessageParam.builder() .role(MessageParam.Role.USER) .content("Schedule an all-hands with everyone: " + emails) .build()); Message response = client.messages().create(MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addTool(calendarTool) .addTool(listTool) .messages(messages) .build()); while (response.stopReason().isPresent() && response.stopReason().get().equals(StopReason.TOOL_USE)) { List toolResults = new ArrayList<>(); for (ContentBlock block : response.content()) { if (block.toolUse().isPresent()) { ToolUseBlock toolUse = block.toolUse().get(); ToolResultBlockParam.Builder resultBuilder = ToolResultBlockParam.builder() .toolUseId(toolUse.id()); try { resultBuilder.content(runTool(toolUse)); } catch (Exception e) { // Signal failure so Claude can retry or ask for clarification. resultBuilder.content(e.getMessage()).isError(true); } toolResults.add(ContentBlockParam.ofToolResult(resultBuilder.build())); } } messages.add(response.toParam()); messages.add(MessageParam.builder() .role(MessageParam.Role.USER) .contentOfBlockParams(toolResults) .build()); response = client.messages().create(MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addTool(calendarTool) .addTool(listTool) .messages(messages) .build()); } response.content().stream() .flatMap(block -> block.text().stream()) .forEach(textBlock -> IO.println(textBlock.text())); } ``` ```php PHP 'create_calendar_event', 'description' => 'Create a calendar event with attendees and optional recurrence.', 'input_schema' => [ 'type' => 'object', 'properties' => [ 'title' => ['type' => 'string'], 'start' => ['type' => 'string', 'format' => 'date-time'], 'end' => ['type' => 'string', 'format' => 'date-time'], 'attendees' => [ 'type' => 'array', 'items' => ['type' => 'string', 'format' => 'email'], ], 'recurrence' => [ 'type' => 'object', 'properties' => [ 'frequency' => ['enum' => ['daily', 'weekly', 'monthly']], 'count' => ['type' => 'integer', 'minimum' => 1], ], ], ], 'required' => ['title', 'start', 'end'], ], ], [ 'name' => 'list_calendar_events', 'description' => 'List all calendar events on a given date.', 'input_schema' => [ 'type' => 'object', 'properties' => [ 'date' => ['type' => 'string', 'format' => 'date'], ], 'required' => ['date'], ], ], ]; function runTool(string $name, array $input): string { if ($name === 'create_calendar_event') { if (count($input['attendees'] ?? []) > 10) { throw new InvalidArgumentException('Too many attendees (max 10)'); } return json_encode([ 'event_id' => 'evt_123', 'status' => 'created', 'title' => $input['title'], ]); } if ($name === 'list_calendar_events') { return json_encode([ 'events' => [['title' => 'Existing meeting', 'start' => '14:00', 'end' => '15:00']], ]); } throw new InvalidArgumentException("Unknown tool: {$name}"); } // Build a request that exceeds the tool's attendee limit so the error path runs. $emails = array_map(fn (int $i): string => "user{$i}@example.com", range(0, 14)); $messages = [ [ 'role' => 'user', 'content' => 'Schedule an all-hands with everyone: ' . implode(', ', $emails), ], ]; $response = $client->messages->create( model: 'claude-opus-5', maxTokens: 1024, tools: $tools, messages: $messages, ); while ($response->stopReason === 'tool_use') { $toolResults = []; foreach ($response->content as $block) { if ($block->type === 'tool_use') { try { $toolResults[] = [ 'type' => 'tool_result', 'tool_use_id' => $block->id, 'content' => runTool($block->name, $block->input), ]; } catch (Exception $e) { // Signal failure so Claude can retry or ask for clarification. $toolResults[] = [ 'type' => 'tool_result', 'tool_use_id' => $block->id, 'content' => $e->getMessage(), 'is_error' => true, ]; } } } $messages[] = ['role' => 'assistant', 'content' => $response->content]; $messages[] = ['role' => 'user', 'content' => $toolResults]; $response = $client->messages->create( model: 'claude-opus-5', maxTokens: 1024, tools: $tools, messages: $messages, ); } foreach ($response->content as $block) { if ($block->type === 'text') { echo $block->text, "\n"; } } ``` ```ruby Ruby # Ring 4: Error handling. require "anthropic" client = Anthropic::Client.new tools = [ { name: "create_calendar_event", description: "Create a calendar event with attendees and optional recurrence.", input_schema: { type: "object", properties: { title: {type: "string"}, start: {type: "string", format: "date-time"}, end: {type: "string", format: "date-time"}, attendees: { type: "array", items: {type: "string", format: "email"} }, recurrence: { type: "object", properties: { frequency: {enum: ["daily", "weekly", "monthly"]}, count: {type: "integer", minimum: 1} } } }, required: ["title", "start", "end"] } }, { name: "list_calendar_events", description: "List all calendar events on a given date.", input_schema: { type: "object", properties: { date: {type: "string", format: "date"} }, required: ["date"] } } ] def run_tool(name, input) case name when "create_calendar_event" attendees = input[:attendees] raise ArgumentError, "Too many attendees (max 10)" if attendees && attendees.length > 10 JSON.generate({event_id: "evt_123", status: "created", title: input[:title]}) when "list_calendar_events" JSON.generate({events: [{title: "Existing meeting", start: "14:00", end: "15:00"}]}) else raise ArgumentError, "Unknown tool: #{name}" end end # Build a request that exceeds the tool's attendee limit so the error path runs. emails = (0...15).map { |i| "user#{i}@example.com" } messages = [ { role: "user", content: "Schedule an all-hands with everyone: #{emails.join(", ")}" } ] response = client.messages.create( model: "claude-opus-5", max_tokens: 1024, tools: tools, messages: messages ) while response.stop_reason == :tool_use tool_results = response.content.select { |block| block.type == :tool_use }.map do |tool_use| begin { type: "tool_result", tool_use_id: tool_use.id, content: run_tool(tool_use.name, tool_use.input) } rescue => e # Signal failure so Claude can retry or ask for clarification. { type: "tool_result", tool_use_id: tool_use.id, content: e.message, is_error: true } end end messages << {role: "assistant", content: response.content} messages << {role: "user", content: tool_results} response = client.messages.create( model: "claude-opus-5", max_tokens: 1024, tools: tools, messages: messages ) end response.content.each do |block| puts block.text if block.type == :text end ``` **What to expect** ```text Output wrap I tried to schedule the all-hands but the calendar only allows 10 attendees per event. I can split this into two sessions, or you can let me know which 10 people to prioritize. ``` The `is_error` flag is the only difference from a successful result. Claude sees the flag and the error text, and responds accordingly. See [Handle tool calls](https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls) for the full error-handling reference. ## Ring 5: The Tool Runner SDK abstraction Rings 2 through 4 wrote the same loop by hand: call the API, check `stop_reason`, run tools, append results, repeat. The Tool Runner does this for you. Define each tool as a function, pass the list to `tool_runner`, and retrieve the final message once the loop completes. Error wrapping, result formatting, and conversation management are handled internally. Each SDK provides a helper that turns an ordinary function into a runnable tool and derives the input schema from its signature; the tabs below show the idiomatic form for each language. Tool Runner is available in all seven SDKs: Python, TypeScript, C#, Go, Java, PHP, and Ruby. See [Tool Runner](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-runner) for the full reference. The cURL and CLI tabs show a note instead of code; keep the Ring 4 loop for curl- or CLI-based scripts. ```bash cURL #!/bin/bash # Ring 5: The Tool Runner SDK abstraction. # The Tool Runner SDK abstraction is available in all seven SDKs: Python, # TypeScript, C#, Go, Java, PHP, and Ruby. There is no equivalent for raw # curl requests. Switch to any SDK tab to see Ring 5, or keep the Ring 4 # loop as your shell implementation. ``` ```bash CLI #!/usr/bin/env bash # Ring 5: The Tool Runner SDK abstraction. set -euo pipefail # The Tool Runner SDK abstraction is available in all seven SDKs: Python, # TypeScript, C#, Go, Java, PHP, and Ruby. The ant CLI exposes the Messages # API directly and has no equivalent helper. Switch to any SDK tab to see # Ring 5, or keep the Ring 4 loop as your CLI implementation. ``` ```python Python # Ring 5: The Tool Runner SDK abstraction. import json import anthropic from anthropic import beta_tool client = anthropic.Anthropic() @beta_tool def create_calendar_event( title: str, start: str, end: str, attendees: list[str] | None = None, recurrence: dict | None = None, ) -> str: """Create a calendar event with attendees and optional recurrence. Args: title: Event title. start: Start time in ISO 8601 format. end: End time in ISO 8601 format. attendees: Email addresses to invite. recurrence: Dict with 'frequency' (daily, weekly, monthly) and 'count'. """ if attendees and len(attendees) > 10: raise ValueError("Too many attendees (max 10)") return json.dumps({"event_id": "evt_123", "status": "created", "title": title}) @beta_tool def list_calendar_events(date: str) -> str: """List all calendar events on a given date. Args: date: Date in YYYY-MM-DD format. """ return json.dumps({"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}) final_message = client.beta.messages.tool_runner( model="claude-opus-5", max_tokens=1024, tools=[create_calendar_event, list_calendar_events], messages=[ { "role": "user", "content": "Check what I have next Monday, then schedule a planning session that avoids any conflicts.", } ], ).until_done() for block in final_message.content: if block.type == "text": print(block.text) ``` ```typescript TypeScript // Ring 5: The Tool Runner SDK abstraction. import Anthropic from "@anthropic-ai/sdk"; import { betaZodTool } from "@anthropic-ai/sdk/helpers/beta/zod"; import { z } from "zod"; const client = new Anthropic(); const createCalendarEvent = betaZodTool({ name: "create_calendar_event", description: "Create a calendar event with attendees and optional recurrence.", inputSchema: z.object({ title: z.string(), start: z.string().datetime(), end: z.string().datetime(), attendees: z.array(z.string().email()).optional(), recurrence: z .object({ frequency: z.enum(["daily", "weekly", "monthly"]), count: z.number().int().min(1), }) .optional(), }), run: async (input) => { if (input.attendees && input.attendees.length > 10) { throw new Error("Too many attendees (max 10)"); } return JSON.stringify({ event_id: "evt_123", status: "created", title: input.title, }); }, }); const listCalendarEvents = betaZodTool({ name: "list_calendar_events", description: "List all calendar events on a given date.", inputSchema: z.object({ date: z.string().date(), }), run: async () => { return JSON.stringify({ events: [{ title: "Existing meeting", start: "14:00", end: "15:00" }], }); }, }); const finalMessage = await client.beta.messages.toolRunner({ model: "claude-opus-5", max_tokens: 1024, tools: [createCalendarEvent, listCalendarEvents], messages: [ { role: "user", content: "Check what I have next Monday, then schedule a planning session that avoids any conflicts.", }, ], }); for (const block of finalMessage.content) { if (block.type === "text") { console.log(block.text); } } ``` ```csharp C# // Ring 5: The Tool Runner SDK abstraction. using System; using System.Collections.Generic; using System.Text.Json; using System.Threading.Tasks; using Anthropic; using Anthropic.Helpers.Beta; using Anthropic.Models.Beta.Messages; using MessageCreateParams = Anthropic.Models.Beta.Messages.MessageCreateParams; using InputSchema = Anthropic.Models.Beta.Messages.InputSchema; using Role = Anthropic.Models.Beta.Messages.Role; using Model = Anthropic.Models.Messages.Model; AnthropicClient client = new(); // Define each tool as a runnable tool: the definition carries the JSON Schema // and the Run callback holds the implementation. Throwing an exception sends // the message back to Claude as a tool result with is_error set. var createCalendarEvent = new BetaRunnableTool { Name = "create_calendar_event", Definition = new BetaTool { Name = "create_calendar_event", Description = "Create a calendar event with attendees and optional recurrence.", InputSchema = new InputSchema { Properties = new Dictionary { ["title"] = JsonSerializer.SerializeToElement(new { type = "string", description = "Event title" }), ["start"] = JsonSerializer.SerializeToElement(new { type = "string", description = "Start time in ISO 8601 format" }), ["end"] = JsonSerializer.SerializeToElement(new { type = "string", description = "End time in ISO 8601 format" }), ["attendees"] = JsonSerializer.SerializeToElement(new { type = "array", items = new { type = "string" }, description = "Email addresses to invite", }), ["recurrence"] = JsonSerializer.SerializeToElement(new { type = "object", properties = new { frequency = new { @enum = new[] { "daily", "weekly", "monthly" } }, count = new { type = "integer", minimum = 1 }, }, }), }, Required = ["title", "start", "end"], }, }, Run = (toolUse, _) => { if (toolUse.Input.TryGetValue("attendees", out var attendees) && attendees.GetArrayLength() > 10) { throw new InvalidOperationException("Too many attendees (max 10)"); } var title = toolUse.Input.TryGetValue("title", out var t) ? t.GetString() : ""; return Task.FromResult( JsonSerializer.Serialize(new { event_id = "evt_123", status = "created", title }) ); }, }; var listCalendarEvents = new BetaRunnableTool { Name = "list_calendar_events", Definition = new BetaTool { Name = "list_calendar_events", Description = "List all calendar events on a given date.", InputSchema = new InputSchema { Properties = new Dictionary { ["date"] = JsonSerializer.SerializeToElement(new { type = "string", description = "Date in YYYY-MM-DD format" }), }, Required = ["date"], }, }, Run = (toolUse, _) => Task.FromResult( """{"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}""" ), }; // The runner calls the API, runs requested tools, and feeds results back // until Claude produces a final answer. var runner = client.Beta.Messages.ToolRunner( new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [ new() { Role = Role.User, Content = "Check what I have next Monday, then schedule a planning session that avoids any conflicts.", }, ], }, [createCalendarEvent, listCalendarEvents] ); BetaMessage? finalMessage = null; await foreach (var message in runner) { finalMessage = message; } foreach (var block in finalMessage!.Content) { if (block.TryPickText(out var text)) { Console.WriteLine(text.Text); } } ``` ```go Go // Ring 5: The Tool Runner SDK abstraction. package main import ( "context" "fmt" "log" "github.com/anthropics/anthropic-sdk-go" "github.com/anthropics/anthropic-sdk-go/toolrunner" ) // The input structs define each tool's schema. The tool runner generates the // JSON Schema from the struct fields and their jsonschema tags. type RecurrenceInput struct { Frequency string `json:"frequency,omitempty" jsonschema:"enum=daily,enum=weekly,enum=monthly,description=How often the event repeats"` Count int `json:"count,omitempty" jsonschema:"description=Number of occurrences"` } type CreateCalendarEventInput struct { Title string `json:"title" jsonschema:"required,description=Event title"` Start string `json:"start" jsonschema:"required,description=Start time in ISO 8601 format"` End string `json:"end" jsonschema:"required,description=End time in ISO 8601 format"` Attendees []string `json:"attendees,omitempty" jsonschema:"description=Email addresses to invite"` Recurrence *RecurrenceInput `json:"recurrence,omitempty"` } type ListCalendarEventsInput struct { Date string `json:"date" jsonschema:"required,description=Date in YYYY-MM-DD format"` } func main() { client := anthropic.NewClient() ctx := context.Background() // Define each tool as a handler function. Returning an error sends the // message back to Claude as a tool result with is_error set. createCalendarEvent, err := toolrunner.NewBetaToolFromJSONSchema( "create_calendar_event", "Create a calendar event with attendees and optional recurrence.", func(ctx context.Context, input CreateCalendarEventInput) (anthropic.BetaToolResultBlockParamContentUnion, error) { if len(input.Attendees) > 10 { return anthropic.BetaToolResultBlockParamContentUnion{}, fmt.Errorf("too many attendees (max 10)") } return anthropic.BetaToolResultBlockParamContentUnion{ OfText: &anthropic.BetaTextBlockParam{ Text: fmt.Sprintf(`{"event_id": "evt_123", "status": "created", "title": %q}`, input.Title), }, }, nil }, ) if err != nil { log.Fatal(err) } listCalendarEvents, err := toolrunner.NewBetaToolFromJSONSchema( "list_calendar_events", "List all calendar events on a given date.", func(ctx context.Context, input ListCalendarEventsInput) (anthropic.BetaToolResultBlockParamContentUnion, error) { return anthropic.BetaToolResultBlockParamContentUnion{ OfText: &anthropic.BetaTextBlockParam{ Text: `{"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}`, }, }, nil }, ) if err != nil { log.Fatal(err) } // The runner calls the API, runs requested tools, and feeds results back // until Claude produces a final answer. runner := client.Beta.Messages.NewToolRunner( []anthropic.BetaTool{createCalendarEvent, listCalendarEvents}, anthropic.BetaToolRunnerParams{ BetaMessageNewParams: anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock( "Check what I have next Monday, then schedule a planning session that avoids any conflicts.", )), }, }, }, ) var finalMessage *anthropic.BetaMessage for message, err := range runner.All(ctx) { if err != nil { log.Fatal(err) } finalMessage = message } for _, block := range finalMessage.Content { if block.Type == "text" { fmt.Println(block.Text) } } } ``` ```java Java // Ring 5: The Tool Runner SDK abstraction. import com.anthropic.client.AnthropicClient; import com.anthropic.client.okhttp.AnthropicOkHttpClient; import com.anthropic.helpers.BetaToolRunner; import com.anthropic.models.beta.messages.BetaMessage; import com.anthropic.models.beta.messages.MessageCreateParams; import com.anthropic.models.messages.Model; import com.fasterxml.jackson.annotation.JsonClassDescription; import com.fasterxml.jackson.annotation.JsonPropertyDescription; import java.util.List; import java.util.function.Supplier; // Define each tool as a class: the fields describe the input schema, and the // get() method holds the implementation. Throwing an exception sends the // message back to Claude as a tool result with is_error set. @JsonClassDescription("Create a calendar event with attendees.") static class CreateCalendarEvent implements Supplier { @JsonPropertyDescription("Event title") public String title; @JsonPropertyDescription("Start time in ISO 8601 format") public String start; @JsonPropertyDescription("End time in ISO 8601 format") public String end; @JsonPropertyDescription("Email addresses to invite") public List attendees; @Override public String get() { if (attendees != null && attendees.size() > 10) { throw new IllegalArgumentException("Too many attendees (max 10)"); } return "{\"event_id\": \"evt_123\", \"status\": \"created\", \"title\": \"" + title + "\"}"; } } @JsonClassDescription("List all calendar events on a given date.") static class ListCalendarEvents implements Supplier { @JsonPropertyDescription("Date in YYYY-MM-DD format") public String date; @Override public String get() { return "{\"events\": [{\"title\": \"Existing meeting\", \"start\": \"14:00\", \"end\": \"15:00\"}]}"; } } void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); // The runner calls the API, runs requested tools, and feeds results back // until Claude produces a final answer. BetaToolRunner runner = client.beta() .messages() .toolRunner(MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .addBeta("structured-outputs-2025-11-13") .addUserMessage("Check what I have next Monday, then schedule a planning session that avoids any conflicts.") .addTool(CreateCalendarEvent.class) .addTool(ListCalendarEvents.class) .build()); BetaMessage finalMessage = null; for (BetaMessage message : runner) { finalMessage = message; } finalMessage.content().stream() .flatMap(block -> block.text().stream()) .forEach(textBlock -> IO.println(textBlock.text())); } ``` ```php PHP 'create_calendar_event', 'description' => 'Create a calendar event with attendees and optional recurrence.', 'input_schema' => [ 'type' => 'object', 'properties' => [ 'title' => ['type' => 'string', 'description' => 'Event title'], 'start' => ['type' => 'string', 'description' => 'Start time in ISO 8601 format'], 'end' => ['type' => 'string', 'description' => 'End time in ISO 8601 format'], 'attendees' => [ 'type' => 'array', 'items' => ['type' => 'string'], 'description' => 'Email addresses to invite', ], 'recurrence' => [ 'type' => 'object', 'properties' => [ 'frequency' => ['enum' => ['daily', 'weekly', 'monthly']], 'count' => ['type' => 'integer', 'minimum' => 1], ], ], ], 'required' => ['title', 'start', 'end'], ], ], run: function (array $input): string { if (count($input['attendees'] ?? []) > 10) { throw new InvalidArgumentException('Too many attendees (max 10)'); } return json_encode([ 'event_id' => 'evt_123', 'status' => 'created', 'title' => $input['title'], ]); }, ); $listCalendarEvents = new BetaRunnableTool( definition: [ 'name' => 'list_calendar_events', 'description' => 'List all calendar events on a given date.', 'input_schema' => [ 'type' => 'object', 'properties' => [ 'date' => ['type' => 'string', 'description' => 'Date in YYYY-MM-DD format'], ], 'required' => ['date'], ], ], run: fn (array $input): string => json_encode([ 'events' => [['title' => 'Existing meeting', 'start' => '14:00', 'end' => '15:00']], ]), ); // The runner calls the API, runs requested tools, and feeds results back // until Claude produces a final answer. $runner = $client->beta->messages->toolRunner( maxTokens: 1024, messages: [ [ 'role' => 'user', 'content' => 'Check what I have next Monday, then schedule a planning session that avoids any conflicts.', ], ], model: Model::CLAUDE_OPUS_5, tools: [$createCalendarEvent, $listCalendarEvents], ); $finalMessage = null; foreach ($runner as $message) { $finalMessage = $message; } foreach ($finalMessage->content as $block) { if ($block->type === 'text') { echo $block->text, "\n"; } } ``` ```ruby Ruby # Ring 5: The Tool Runner SDK abstraction. require "anthropic" client = Anthropic::Client.new # Define each tool as a class: a typed input model describes the schema, and # the call method holds the implementation. Raising an error sends the message # back to Claude as a tool result with is_error set. class RecurrenceInput < Anthropic::BaseModel optional :frequency, Anthropic::InputSchema::EnumOf["daily", "weekly", "monthly"], doc: "How often the event repeats" optional :count, Integer, doc: "Number of occurrences" end class CreateCalendarEventInput < Anthropic::BaseModel required :title, String, doc: "Event title" required :start, String, doc: "Start time in ISO 8601 format" required :end, String, doc: "End time in ISO 8601 format" optional :attendees, Anthropic::InputSchema::ArrayOf[String], doc: "Email addresses to invite" optional :recurrence, RecurrenceInput, doc: "Optional recurrence rule" end class CreateCalendarEvent < Anthropic::BaseTool doc "Create a calendar event with attendees and optional recurrence." input_schema CreateCalendarEventInput def call(input) raise ArgumentError, "Too many attendees (max 10)" if input.attendees && input.attendees.length > 10 JSON.generate({event_id: "evt_123", status: "created", title: input.title}) end end class ListCalendarEventsInput < Anthropic::BaseModel required :date, String, doc: "Date in YYYY-MM-DD format" end class ListCalendarEvents < Anthropic::BaseTool doc "List all calendar events on a given date." input_schema ListCalendarEventsInput def call(input) JSON.generate({events: [{title: "Existing meeting", start: "14:00", end: "15:00"}]}) end end # The runner calls the API, runs requested tools, and feeds results back # until Claude produces a final answer. runner = client.beta.messages.tool_runner( model: "claude-opus-5", max_tokens: 1024, tools: [CreateCalendarEvent.new, ListCalendarEvents.new], messages: [ { role: "user", content: "Check what I have next Monday, then schedule a planning session that avoids any conflicts." } ] ) final_message = nil runner.each_message { |message| final_message = message } final_message.content.each do |block| puts block.text if block.type == :text end ``` **What to expect** ```text Output wrap I checked your calendar for next Monday and found an existing meeting from 2pm to 3pm. I've scheduled the planning session for 10am to 11am to avoid the conflict. ``` The output is identical to Ring 3. The difference is in the code: roughly half the lines, no manual loop, and the schema lives next to the implementation. ## What you built You started with a single hardcoded tool call and ended with a production-shaped agent that handles multiple tools, parallel calls, and errors, then collapsed all of that into the Tool Runner. Along the way you saw every piece of the tool-use protocol: `tool_use` blocks, `tool_result` blocks, `tool_use_id` matching, `stop_reason` checking, and `is_error` signaling. ## Next steps Schema specification and best practices. The full SDK abstraction reference. Fix common tool-use errors. --- title: Web fetch tool url: https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-fetch-tool description: Fetch and read content from specific URLs to augment Claude's context with live web content. --- For how zero data retention (ZDR) applies to this feature, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention). The web fetch tool allows Claude to retrieve full content from specified web pages and PDF documents. The latest web fetch tool version (`web_fetch_20260318`) supports **dynamic filtering** with Claude Fable 5, Claude Opus 4.8, Claude Mythos 5, [Claude Mythos Preview](https://anthropic.com/glasswing), Claude Opus 4.7, Claude Opus 4.6, Claude Sonnet 5, and Claude Sonnet 4.6. Claude can write and execute code to filter fetched content before it reaches the context window, keeping only relevant information and discarding the rest. This reduces token consumption while maintaining response quality. `web_fetch_20260318` also adds [response inclusion](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-fetch-tool#response-inclusion) control for agentic workflows. The previous versions (`web_fetch_20260309` for dynamic filtering and [cache bypass](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-fetch-tool#cache-bypass), `web_fetch_20260209` for dynamic filtering only, `web_fetch_20250910` for basic fetch) remain available. Web fetch (with and without dynamic filtering) is available on the Claude API, [Claude Platform on AWS](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws), and [Microsoft Foundry](https://platform.claude.com/docs/en/build-with-claude/claude-in-microsoft-foundry). On Microsoft Foundry, web fetch requires a [Hosted on Anthropic deployment](https://platform.claude.com/docs/en/build-with-claude/claude-in-microsoft-foundry#additional-features-not-supported-when-hosted-on-azure). It is not currently available on Amazon Bedrock or Google Cloud. For [Claude Mythos Preview](https://anthropic.com/glasswing), web fetch is available on the Claude API and Microsoft Foundry. It is not currently available for Mythos Preview on Amazon Bedrock or Google Cloud. Use the [feedback form](https://forms.gle/NhWcgmkcvPCMmPE86) to provide feedback on the quality of the model responses, the API itself, or the quality of the documentation. For Zero Data Retention eligibility and the `allowed_callers` workaround, see [Server tools](https://platform.claude.com/docs/en/agents-and-tools/tool-use/server-tools#zdr-and-allowed-callers). Enabling the web fetch tool in environments where Claude processes untrusted input alongside sensitive data poses data exfiltration risks. Only use this tool in trusted environments or when handling non-sensitive data. To minimize exfiltration risks, Claude is not allowed to dynamically construct URLs. Claude can only fetch URLs that have been explicitly provided by the user or that come from previous web search or web fetch results. However, there is still residual risk that you should carefully consider when using this tool. If data exfiltration is a concern, consider: * Disabling the web fetch tool entirely * Using the `max_uses` parameter to limit the number of requests * Using the `allowed_domains` parameter to restrict to known safe domains For model support, see the [Tool reference](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-reference). ## How web fetch works Web fetch is a [server tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/server-tools): the API fetches the content during the request and inserts the results into the conversation. You don't run anything or return a `tool_result`. The exception is when Claude calls web fetch and one of your client tools in the same group of parallel tool calls: the API returns the response with `stop_reason: "tool_use"` before that fetch has run, then runs the fetch when you send back the client `tool_result` blocks. See [Mixing server tools and client tools in one turn](https://platform.claude.com/docs/en/agents-and-tools/tool-use/server-tools#mixing-server-tools-and-client-tools-in-one-turn). When you add the web fetch tool to your API request: 1. Claude determines when to fetch content based on the prompt and available URLs. 2. The API retrieves the full text content from the specified URL. 3. For PDFs, the API returns the content as base64-encoded data and processes it like a directly attached PDF document. 4. Claude analyzes the fetched content and provides a response with optional citations. The web fetch tool currently does not support websites dynamically rendered with JavaScript. ### When Claude fetches Claude fetches when the request points at a specific page or document: * A URL is provided in the conversation (or a previous tool result) * The user names a specific resource (a particular article, README, pricing page, or documentation section) without a URL, and the [web search tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-search-tool) is also enabled so Claude can locate it first (see [Combined search and fetch](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-fetch-tool#combined-search-and-fetch)) Claude does **not** fetch for general-knowledge or open-ended questions that don't reference a specific page. "Summarize this article: ``" triggers a fetch. "What are best practices for REST API design?" is answered directly. ### Dynamic filtering Fetching full web pages and PDFs can quickly consume tokens, especially when only specific information is needed from large documents. With `web_fetch_20260209` or later, Claude can write and execute code to filter the fetched content before loading it into context. This dynamic filtering is particularly useful for: * Extracting specific sections from long documents * Processing structured data from web pages * Filtering relevant information from PDFs * Reducing token costs when working with large documents Dynamic filtering runs on the [code execution tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool), which the API enables automatically for the request. You don't need to add the code execution tool to the `tools` array. To enable dynamic filtering, use `web_fetch_20260209` or any later version. The following examples use `web_fetch_20260318`: ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-4-8", "max_tokens": 4096, "messages": [ { "role": "user", "content": "Fetch the content at https://example.com/research-paper and extract the key findings." } ], "tools": [{ "type": "web_fetch_20260318", "name": "web_fetch" }] }' ``` ```bash CLI ant messages create <<'YAML' model: claude-opus-4-8 max_tokens: 4096 messages: - role: user content: >- Fetch the content at https://example.com/research-paper and extract the key findings. tools: - type: web_fetch_20260318 name: web_fetch YAML ``` ```python Python client = anthropic.Anthropic() response = client.messages.create( model="claude-opus-4-8", max_tokens=4096, messages=[ { "role": "user", "content": "Fetch the content at https://example.com/research-paper and extract the key findings.", } ], tools=[{"type": "web_fetch_20260318", "name": "web_fetch"}], ) print(response) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.messages.create({ model: "claude-opus-4-8", max_tokens: 4096, messages: [ { role: "user", content: "Fetch the content at https://example.com/research-paper and extract the key findings." } ], tools: [{ type: "web_fetch_20260318", name: "web_fetch" }] }); console.log(response); ``` ```csharp C# AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus4_8, MaxTokens = 4096, Messages = [new() { Role = Role.User, Content = "Fetch the content at https://example.com/research-paper and extract the key findings." }], Tools = [new ToolUnion(new WebFetchTool20260318())] }; var message = await client.Messages.Create(parameters); Console.WriteLine(message); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus4_8, MaxTokens: 4096, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Fetch the content at https://example.com/research-paper and extract the key findings.")), }, Tools: []anthropic.ToolUnionParam{ {OfWebFetchTool20260318: &anthropic.WebFetchTool20260318Param{}}, }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java import com.anthropic.models.messages.WebFetchTool20260318; void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_4_8) .maxTokens(4096L) .addUserMessage("Fetch the content at https://example.com/research-paper and extract the key findings.") .addTool(WebFetchTool20260318.builder().build()) .build(); Message response = client.messages().create(params); IO.println(response); } ``` ```php PHP $client = new Client(); $message = $client->messages->create( maxTokens: 4096, messages: [ ['role' => 'user', 'content' => 'Fetch the content at https://example.com/research-paper and extract the key findings.'] ], model: 'claude-opus-4-8', tools: [[ 'type' => 'web_fetch_20260318', 'name' => 'web_fetch', ]], ); echo $message; ``` ```ruby Ruby client = Anthropic::Client.new message = client.messages.create( model: "claude-opus-4-8", max_tokens: 4096, messages: [ { role: "user", content: "Fetch the content at https://example.com/research-paper and extract the key findings." } ], tools: [{ type: "web_fetch_20260318", name: "web_fetch" }] ) puts message ``` ## How to use web fetch Provide the web fetch tool in your API request: ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-4-8", "max_tokens": 1024, "messages": [ { "role": "user", "content": "Please analyze the content at https://example.com/article" } ], "tools": [{ "type": "web_fetch_20250910", "name": "web_fetch", "max_uses": 5 }] }' ``` ```bash CLI ant messages create \ --model claude-opus-4-8 \ --max-tokens 1024 \ --message '{role: user, content: "Please analyze the content at https://example.com/article"}' \ --tool '{type: web_fetch_20250910, name: web_fetch, max_uses: 5}' ``` ```python Python client = anthropic.Anthropic() response = client.messages.create( model="claude-opus-4-8", max_tokens=1024, messages=[ { "role": "user", "content": "Please analyze the content at https://example.com/article", } ], tools=[{"type": "web_fetch_20250910", "name": "web_fetch", "max_uses": 5}], ) print(response) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.messages.create({ model: "claude-opus-4-8", max_tokens: 1024, messages: [ { role: "user", content: "Please analyze the content at https://example.com/article" } ], tools: [ { type: "web_fetch_20250910", name: "web_fetch", max_uses: 5 } ] }); console.log(response); ``` ```csharp C# AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus4_8, MaxTokens = 1024, Messages = [new() { Role = Role.User, Content = "Please analyze the content at https://example.com/article" }], Tools = [new ToolUnion(new WebFetchTool20250910() { MaxUses = 5 })] }; var message = await client.Messages.Create(parameters); Console.WriteLine(message); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus4_8, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Please analyze the content at https://example.com/article")), }, Tools: []anthropic.ToolUnionParam{ {OfWebFetchTool20250910: &anthropic.WebFetchTool20250910Param{ MaxUses: anthropic.Int(5), }}, }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java import com.anthropic.models.messages.WebFetchTool20250910; void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_4_8) .maxTokens(1024L) .addUserMessage("Please analyze the content at https://example.com/article") .addTool(WebFetchTool20250910.builder() .maxUses(5L) .build()) .build(); Message response = client.messages().create(params); IO.println(response); } ``` ```php PHP $client = new Client(); $message = $client->messages->create( maxTokens: 1024, messages: [ ['role' => 'user', 'content' => 'Please analyze the content at https://example.com/article'] ], model: 'claude-opus-4-8', tools: [[ 'type' => 'web_fetch_20250910', 'name' => 'web_fetch', 'max_uses' => 5, ]], ); echo $message; ``` ```ruby Ruby client = Anthropic::Client.new message = client.messages.create( model: "claude-opus-4-8", max_tokens: 1024, messages: [ { role: "user", content: "Please analyze the content at https://example.com/article" } ], tools: [{ type: "web_fetch_20250910", name: "web_fetch", max_uses: 5 }] ) puts message ``` ## Tool definition The web fetch tool supports the following parameters: ```json JSON { "type": "web_fetch_20250910", "name": "web_fetch", // Optional: Limit the number of fetches per request "max_uses": 10, // Optional: Only fetch from these domains "allowed_domains": ["example.com", "docs.example.com"], // Optional: Never fetch from these domains (cannot be combined with allowed_domains) "blocked_domains": ["private.example.com"], // Optional: Enable citations for fetched content "citations": { "enabled": true }, // Optional: Maximum content length in tokens "max_content_tokens": 100000 } ``` Later tool versions add two more optional parameters: `use_cache` requires `web_fetch_20260309` or later (see [Cache bypass](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-fetch-tool#cache-bypass)), and `response_inclusion` requires `web_fetch_20260318` or later (see [Response inclusion](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-fetch-tool#response-inclusion)). ### Max uses The `max_uses` parameter limits the number of web fetches performed. Failed fetches count against the limit. If Claude attempts more fetches than allowed, the `web_fetch_tool_result` is an error with the `max_uses_exceeded` error code. There is currently no default limit. ### Domain filtering For domain filtering with `allowed_domains` and `blocked_domains`, see [Server tools](https://platform.claude.com/docs/en/agents-and-tools/tool-use/server-tools#domain-filtering). ### Content limits The `max_content_tokens` parameter limits the amount of content included in the context. If the fetched content exceeds this limit, the tool truncates it. This helps control token usage when fetching large documents. The limit applies to text content, not to binary content such as PDFs. The `max_content_tokens` parameter limit is approximate. The actual number of input tokens used can vary by a small amount. ### Cache bypass Requires `web_fetch_20260309` or later (including `web_fetch_20260318`). The `use_cache` parameter controls whether cached content may be returned. Set `"use_cache": false` to bypass the cache and fetch fresh content. The default is `true`. Only disable caching when the user explicitly requests fresh content or when fetching rapidly changing sources, because bypassing the cache increases latency. ```json { "tools": [ { "type": "web_fetch_20260309", "name": "web_fetch", "use_cache": false } ] } ``` ### Response inclusion Requires `web_fetch_20260318` or later. The `response_inclusion` parameter controls how fetch result blocks appear in the API response when the result was consumed by a completed [code execution](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool) call in the same turn. Set `"response_inclusion": "excluded"` to drop those nested `server_tool_use` and result block pairs entirely from the response, reducing output token costs for agentic workflows that don't need to echo raw page content back to the client. The default is `"full"`. Results from direct calls, or from code execution calls that paused before completing, are always returned in full so they can be sent back on the next turn. ```json { "tools": [ { "type": "web_fetch_20260318", "name": "web_fetch", "response_inclusion": "excluded" } ] } ``` ### Citations Unlike web search where citations are always enabled, citations are optional for web fetch and disabled by default. Set `"citations": {"enabled": true}` to enable Claude to cite specific passages from fetched documents. When displaying API outputs directly to end users, include citations to the original source. If you are making modifications to API outputs, including by reprocessing and/or combining them with your own material before displaying them to end users, display citations as appropriate based on consultation with your legal team. ## Response Here's an example response structure: ```json Output { "role": "assistant", "content": [ // 1. Claude's decision to fetch { "type": "text", "text": "I'll fetch the content from the article to analyze it." }, // 2. The fetch request { "type": "server_tool_use", "id": "srvtoolu_01234567890abcdef", "name": "web_fetch", "input": { "url": "https://example.com/article" } }, // 3. Fetch results { "type": "web_fetch_tool_result", "tool_use_id": "srvtoolu_01234567890abcdef", "content": { "type": "web_fetch_result", "url": "https://example.com/article", "content": { "type": "document", "source": { "type": "text", "media_type": "text/plain", "data": "Full text content of the article..." }, "title": "Article Title", "citations": { "enabled": true } }, "retrieved_at": "2025-08-25T10:30:00Z" } }, // 4. Claude's analysis with citations (if enabled) { "text": "Based on the article, ", "type": "text" }, { "text": "the main argument presented is that artificial intelligence will transform healthcare", "type": "text", "citations": [ { "type": "char_location", "document_index": 0, "document_title": "Article Title", "start_char_index": 1234, "end_char_index": 1456, "cited_text": "Artificial intelligence is poised to revolutionize healthcare delivery..." } ] } ], "id": "msg_a930390d3a", "usage": { "input_tokens": 25039, "output_tokens": 931, "server_tool_use": { "web_fetch_requests": 1 } }, "stop_reason": "end_turn" } ``` ### Fetch results Fetch results include: * `url`: The URL that was fetched * `content`: A document block containing the fetched content * `retrieved_at`: Timestamp when the content was retrieved The web fetch tool caches results to improve performance and reduce redundant requests. The content returned may not always reflect the latest version available at the URL. The cache behavior is managed automatically and may change over time to optimize for different content types and usage patterns. To fetch fresh content, set `"use_cache": false` (see [Cache bypass](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-fetch-tool#cache-bypass)). For PDF documents, content is returned as base64-encoded data: ```json Output { "type": "web_fetch_tool_result", "tool_use_id": "srvtoolu_02", "content": { "type": "web_fetch_result", "url": "https://example.com/paper.pdf", "content": { "type": "document", "source": { "type": "base64", "media_type": "application/pdf", "data": "JVBERi0xLjQKJcOkw7zDtsOfCjIgMCBvYmo..." }, "citations": { "enabled": true } }, "retrieved_at": "2025-08-25T10:30:02Z" } } ``` ### Errors When the web fetch tool encounters an error, the Claude API returns a 200 (success) response with the error represented in the response body. Claude sees the error result and continues the turn. For example: ```json Output { "type": "web_fetch_tool_result", "tool_use_id": "srvtoolu_a93jad", "content": { "type": "web_fetch_tool_result_error", "error_code": "url_not_accessible" } } ``` These are the possible error codes: * `invalid_tool_input`: Invalid tool input, such as a malformed URL or a non-HTTP(S) scheme * `url_too_long`: URL exceeds maximum length (250 characters) * `url_not_allowed`: URL blocked by domain filtering rules (including your organization's settings) or by Anthropic-side restrictions, such as private addresses and `robots.txt` * `url_not_in_prior_context`: URL did not appear earlier in the conversation (see [URL validation](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-fetch-tool#url-validation)) * `url_not_accessible`: Failed to fetch content (HTTP error) * `too_many_requests`: Rate limit exceeded * `unsupported_content_type`: Content type not supported (only text, HTML, and PDF) * `max_uses_exceeded`: Maximum web fetch tool uses exceeded * `unavailable`: An internal error occurred ## URL validation For security reasons, the web fetch tool can only fetch URLs that have previously appeared in the conversation context. This includes: * URLs in user messages * URLs in client-side tool results * URLs from previous web search or web fetch results The tool cannot fetch arbitrary URLs that Claude generates or URLs from container-based server tools (such as Code Execution and Bash). ## Combined search and fetch When both the web search and web fetch tools are enabled, and the user names a specific page or document without providing a URL (for example, "read the README from the anthropics/anthropic-sdk-python repository"), Claude uses web search to locate it, then fetches the result. The following example asks for a search and an analysis in one request: ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-4-8", "max_tokens": 4096, "messages": [ { "role": "user", "content": "Find recent articles about quantum computing and analyze the most relevant one in detail" } ], "tools": [ { "type": "web_search_20250305", "name": "web_search", "max_uses": 3 }, { "type": "web_fetch_20250910", "name": "web_fetch", "max_uses": 5, "citations": {"enabled": true} } ] }' ``` ```bash CLI ant messages create <<'YAML' model: claude-opus-4-8 max_tokens: 4096 messages: - role: user content: >- Find recent articles about quantum computing and analyze the most relevant one in detail tools: - type: web_search_20250305 name: web_search max_uses: 3 - type: web_fetch_20250910 name: web_fetch max_uses: 5 citations: enabled: true YAML ``` ```python Python client = anthropic.Anthropic() response = client.messages.create( model="claude-opus-4-8", max_tokens=4096, messages=[ { "role": "user", "content": "Find recent articles about quantum computing and analyze the most relevant one in detail", } ], tools=[ {"type": "web_search_20250305", "name": "web_search", "max_uses": 3}, { "type": "web_fetch_20250910", "name": "web_fetch", "max_uses": 5, "citations": {"enabled": True}, }, ], ) print(response) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.messages.create({ model: "claude-opus-4-8", max_tokens: 4096, messages: [ { role: "user", content: "Find recent articles about quantum computing and analyze the most relevant one in detail" } ], tools: [ { type: "web_search_20250305", name: "web_search", max_uses: 3 }, { type: "web_fetch_20250910", name: "web_fetch", max_uses: 5, citations: { enabled: true } } ] }); console.log(response); ``` ```csharp C# AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus4_8, MaxTokens = 4096, Messages = [new() { Role = Role.User, Content = "Find recent articles about quantum computing and analyze the most relevant one in detail" }], Tools = [ new ToolUnion(new WebSearchTool20250305() { MaxUses = 3 }), new ToolUnion(new WebFetchTool20250910() { MaxUses = 5, Citations = new() { Enabled = true } }) ] }; var message = await client.Messages.Create(parameters); Console.WriteLine(message); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus4_8, MaxTokens: 4096, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Find recent articles about quantum computing and analyze the most relevant one in detail")), }, Tools: []anthropic.ToolUnionParam{ {OfWebSearchTool20250305: &anthropic.WebSearchTool20250305Param{ MaxUses: anthropic.Int(3), }}, {OfWebFetchTool20250910: &anthropic.WebFetchTool20250910Param{ MaxUses: anthropic.Int(5), Citations: anthropic.CitationsConfigParam{Enabled: anthropic.Bool(true)}, }}, }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java import com.anthropic.models.messages.CitationsConfigParam; // ... import com.anthropic.models.messages.WebFetchTool20250910; import com.anthropic.models.messages.WebSearchTool20250305; void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_4_8) .maxTokens(4096L) .addUserMessage("Find recent articles about quantum computing and analyze the most relevant one in detail") .addTool(WebSearchTool20250305.builder() .maxUses(3L) .build()) .addTool(WebFetchTool20250910.builder() .maxUses(5L) .citations(CitationsConfigParam.builder().enabled(true).build()) .build()) .build(); Message response = client.messages().create(params); IO.println(response); } ``` ```php PHP $client = new Client(); $message = $client->messages->create( maxTokens: 4096, messages: [ ['role' => 'user', 'content' => 'Find recent articles about quantum computing and analyze the most relevant one in detail'] ], model: 'claude-opus-4-8', tools: [ [ 'type' => 'web_search_20250305', 'name' => 'web_search', 'max_uses' => 3, ], [ 'type' => 'web_fetch_20250910', 'name' => 'web_fetch', 'max_uses' => 5, 'citations' => ['enabled' => true], ], ], ); echo $message; ``` ```ruby Ruby client = Anthropic::Client.new message = client.messages.create( model: "claude-opus-4-8", max_tokens: 4096, messages: [ { role: "user", content: "Find recent articles about quantum computing and analyze the most relevant one in detail" } ], tools: [ { type: "web_search_20250305", name: "web_search", max_uses: 3 }, { type: "web_fetch_20250910", name: "web_fetch", max_uses: 5, citations: { enabled: true } } ] ) puts message ``` In this workflow, Claude: 1. Uses web search to find relevant articles. 2. Selects the most promising results. 3. Uses web fetch to retrieve full content. 4. Provides detailed analysis with citations. ## Prompt caching For caching tool definitions across turns, see [Tool use with prompt caching](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-use-with-prompt-caching). ## Streaming With streaming enabled, fetch events are part of the stream with a pause during content retrieval: ```sse Output event: message_start data: {"type": "message_start", "message": {"id": "msg_abc123", "type": "message"}} event: content_block_start data: {"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}} // Claude's decision to fetch event: content_block_start data: {"type": "content_block_start", "index": 1, "content_block": {"type": "server_tool_use", "id": "srvtoolu_xyz789", "name": "web_fetch"}} // Fetch URL streamed event: content_block_delta data: {"type": "content_block_delta", "index": 1, "delta": {"type": "input_json_delta", "partial_json": "{\"url\":\"https://example.com/article\"}"}} // Pause while fetch executes // Fetch results streamed event: content_block_start data: {"type": "content_block_start", "index": 2, "content_block": {"type": "web_fetch_tool_result", "tool_use_id": "srvtoolu_xyz789", "content": {"type": "web_fetch_result", "url": "https://example.com/article", "content": {"type": "document", "source": {"type": "text", "media_type": "text/plain", "data": "Article content..."}}}}} // Claude's response continues... ``` ## Batch requests You can include the web fetch tool in the [Messages Batches API](https://platform.claude.com/docs/en/build-with-claude/batch-processing). Web fetch tool calls through the Messages Batches API are priced the same as those in regular Messages API requests. ## Usage and pricing Web fetch usage has **no additional charges** beyond standard token costs: ```json { "usage": { "input_tokens": 25039, "output_tokens": 931, "cache_read_input_tokens": 0, "cache_creation_input_tokens": 0, "server_tool_use": { "web_fetch_requests": 1 } } } ``` The web fetch tool is available on the Claude API at **no additional cost**. You only pay standard token costs for the fetched content that becomes part of your conversation context. To protect against inadvertently fetching large content that would consume excessive tokens, use the `max_content_tokens` parameter to set appropriate limits based on your use case and budget considerations. Example token usage for typical content: * Average web page (10 kB): \~2,500 tokens * Large documentation page (100 kB): \~25,000 tokens * Research paper PDF (500 kB): \~125,000 tokens ## Next steps Run Python and bash code in a sandboxed container to analyze data, generate files, and iterate on solutions. Work with Anthropic-executed tools: server\_tool\_use blocks, pause\_turn continuation, and domain filtering. Directory of Anthropic-provided tools and reference for optional tool definition properties. --- title: Web search tool url: https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-search-tool description: Give Claude access to current web content with cited sources, optional dynamic filtering, and domain controls. --- For how zero data retention (ZDR) applies to this feature, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention). The web search tool gives Claude direct access to real-time web content, allowing it to answer questions with up-to-date information beyond its knowledge cutoff. The response includes citations for sources drawn from search results. With `web_search_20260209` and later versions, Claude can write and run code that filters the search results before they reach the context window (**dynamic filtering**), keeping only relevant information. Dynamic filtering is available with Claude 4.6 and later models and [Claude Mythos Preview](https://anthropic.com/glasswing). Three versions of the web search tool are available: * `web_search_20250305`: basic web search * `web_search_20260209`: adds [dynamic filtering](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-search-tool#dynamic-filtering) * `web_search_20260318`: adds [response inclusion](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-search-tool#response-inclusion) control for agentic workflows The examples on this page use `web_search_20250305` for basic search and `web_search_20260318` for dynamic filtering. For [Claude Mythos Preview](https://anthropic.com/glasswing), web search is supported on the Claude API, Google Cloud, and Microsoft Foundry. Web search is not available for Mythos Preview on Amazon Bedrock or [Claude Platform on AWS](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws). For web search's Zero Data Retention eligibility and the related `allowed_callers` configuration, see [Server tools](https://platform.claude.com/docs/en/agents-and-tools/tool-use/server-tools#zdr-and-allowed-callers). For model support, see the [Tool reference](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-reference). ## How web search works When you add the web search tool to your API request: 1. Claude determines when to search based on the prompt. 2. The API runs the searches and provides Claude with the results. This process can repeat multiple times throughout a single request. 3. At the end of its turn, Claude provides a final response with cited sources. ### When Claude searches Claude searches when the request depends on information that is current, changing, or outside its training data: * Recent events, news, or announcements * Current prices, rates, scores, or statistics * Information about specific organizations, people, or products that might have changed * Explicit requests to search or look something up Claude answers directly without searching when the request draws on stable knowledge: * Established facts, math, science fundamentals, or coding concepts * Creative writing or brainstorming * Analysis of content already provided in the conversation * Conversational turns and greetings Triggering is steerable through your system prompt: you can encourage Claude to search more readily or to prefer answering directly. For a hard constraint, use `max_uses` to cap the number of searches for each request. ### Dynamic filtering With basic web search, every search result is loaded into Claude's context window, and much of that content can be irrelevant to the request. With `web_search_20260209` or later, Claude instead writes and runs code that filters the results first, so only relevant content reaches the context window. This reduces token use on search-heavy requests. Dynamic filtering runs web search from inside [code execution](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool): on `web_search_20260209` and later, the tool's `allowed_callers` field defaults to `["code_execution_20260120"]`, and when dynamic filtering runs, the API provisions the code execution it needs for the request automatically. You don't need to add the code execution tool to `tools` yourself. There are no additional charges for code execution calls made this way beyond the standard token costs. To call web search directly, without dynamic filtering, set `allowed_callers: ["direct"]`. Models that don't support programmatic tool calling require this setting. Without it, the API returns a 400 error that tells you to set it. The web search tool (with and without dynamic filtering) is available on the Claude API, [Claude Platform on AWS](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws), and [Microsoft Foundry](https://platform.claude.com/docs/en/build-with-claude/claude-in-microsoft-foundry). On Microsoft Foundry, web search requires a [Hosted on Anthropic deployment](https://platform.claude.com/docs/en/build-with-claude/claude-in-microsoft-foundry#additional-features-not-supported-when-hosted-on-azure). On Google Cloud, only the basic web search tool (without dynamic filtering) is available. Web search is not available on Amazon Bedrock. The following examples use `web_search_20260318`: ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 4096, "messages": [ { "role": "user", "content": "Search for the current prices of AAPL and GOOGL, then calculate which has a better P/E ratio." } ], "tools": [{ "type": "web_search_20260318", "name": "web_search" }] }' ``` ```bash CLI ant messages create <<'YAML' model: claude-opus-5 max_tokens: 4096 messages: - role: user content: >- Search for the current prices of AAPL and GOOGL, then calculate which has a better P/E ratio. tools: - type: web_search_20260318 name: web_search YAML ``` ```python Python client = anthropic.Anthropic() response = client.messages.create( model="claude-opus-5", max_tokens=4096, messages=[ { "role": "user", "content": "Search for the current prices of AAPL and GOOGL, then calculate which has a better P/E ratio.", } ], tools=[{"type": "web_search_20260318", "name": "web_search"}], ) print(response) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 4096, messages: [ { role: "user", content: "Search for the current prices of AAPL and GOOGL, then calculate which has a better P/E ratio." } ], tools: [{ type: "web_search_20260318", name: "web_search" }] }); console.log(response); ``` ```csharp C# AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 4096, Messages = [new() { Role = Role.User, Content = "Search for the current prices of AAPL and GOOGL, then calculate which has a better P/E ratio." }], Tools = [new ToolUnion(new WebSearchTool20260318())] }; var message = await client.Messages.Create(parameters); Console.WriteLine(message); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 4096, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Search for the current prices of AAPL and GOOGL, then calculate which has a better P/E ratio.")), }, Tools: []anthropic.ToolUnionParam{ {OfWebSearchTool20260318: &anthropic.WebSearchTool20260318Param{}}, }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java import com.anthropic.models.messages.WebSearchTool20260318; void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(4096L) .addUserMessage("Search for the current prices of AAPL and GOOGL, then calculate which has a better P/E ratio.") .addTool(WebSearchTool20260318.builder().build()) .build(); Message response = client.messages().create(params); IO.println(response); } ``` ```php PHP $client = new Client(); $message = $client->messages->create( maxTokens: 4096, messages: [ ['role' => 'user', 'content' => 'Search for the current prices of AAPL and GOOGL, then calculate which has a better P/E ratio.'], ], model: 'claude-opus-5', tools: [ [ 'type' => 'web_search_20260318', 'name' => 'web_search', ], ], ); echo $message; ``` ```ruby Ruby client = Anthropic::Client.new message = client.messages.create( model: "claude-opus-5", max_tokens: 4096, messages: [ { role: "user", content: "Search for the current prices of AAPL and GOOGL, then calculate which has a better P/E ratio." } ], tools: [{ type: "web_search_20260318", name: "web_search" }] ) puts message ``` ## How to use web search Web search is enabled for your organization unless an administrator has disabled it in the [Claude Console](https://platform.claude.com/settings/privacy), where they can also restrict which domains it searches. If it's disabled, a request that includes the tool fails with a 400 `invalid_request_error` that says web search is not enabled, rather than an [error code](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-search-tool#errors) inside a search result. Provide the web search tool in your API request: ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "messages": [ { "role": "user", "content": "What is the weather in NYC?" } ], "tools": [{ "type": "web_search_20250305", "name": "web_search", "max_uses": 5 }] }' ``` ```bash CLI ant messages create \ --model claude-opus-5 \ --max-tokens 1024 \ --message '{role: user, content: What is the weather in NYC?}' \ --tool '{type: web_search_20250305, name: web_search, max_uses: 5}' ``` ```python Python client = anthropic.Anthropic() response = client.messages.create( model="claude-opus-5", max_tokens=1024, messages=[{"role": "user", "content": "What's the weather in NYC?"}], tools=[{"type": "web_search_20250305", "name": "web_search", "max_uses": 5}], ) print(response) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: "What's the weather in NYC?" } ], tools: [ { type: "web_search_20250305", name: "web_search", max_uses: 5 } ] }); console.log(response); ``` ```csharp C# AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [new() { Role = Role.User, Content = "What's the weather in NYC?" }], Tools = [new ToolUnion(new WebSearchTool20250305() { MaxUses = 5 })] }; var message = await client.Messages.Create(parameters); Console.WriteLine(message); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("What's the weather in NYC?")), }, Tools: []anthropic.ToolUnionParam{ {OfWebSearchTool20250305: &anthropic.WebSearchTool20250305Param{ MaxUses: anthropic.Int(5), }}, }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java import com.anthropic.models.messages.WebSearchTool20250305; void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addUserMessage("What's the weather in NYC?") .addTool(WebSearchTool20250305.builder() .maxUses(5L) .build()) .build(); Message response = client.messages().create(params); IO.println(response); } ``` ```php PHP $client = new Client(); $message = $client->messages->create( maxTokens: 1024, messages: [ ['role' => 'user', 'content' => "What's the weather in NYC?"], ], model: 'claude-opus-5', tools: [ [ 'type' => 'web_search_20250305', 'name' => 'web_search', 'max_uses' => 5, ], ], ); echo $message; ``` ```ruby Ruby client = Anthropic::Client.new message = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: "What's the weather in NYC?" } ], tools: [{ type: "web_search_20250305", name: "web_search", max_uses: 5 }] ) puts message ``` ## Tool definition The web search tool supports the following parameters: ```json JSON { "type": "web_search_20250305", "name": "web_search", // Optional: Limit the number of searches per request "max_uses": 5, // Optional: Only include results from these domains. // Use allowed_domains or blocked_domains, not both. "allowed_domains": ["example.com", "trusteddomain.org"], // Optional: Never include results from these domains "blocked_domains": ["untrustedsource.com"], // Optional: Localize search results "user_location": { "type": "approximate", "city": "San Francisco", "region": "California", "country": "US", "timezone": "America/Los_Angeles" } } ``` All web search tool versions accept `allowed_callers`, which controls whether Claude calls web search directly or from code execution through [dynamic filtering](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-search-tool#dynamic-filtering). On `web_search_20260209` and later it defaults to `["code_execution_20260120"]` instead of `["direct"]`. See [Server tools](https://platform.claude.com/docs/en/agents-and-tools/tool-use/server-tools#zdr-and-allowed-callers) for how to configure it. `web_search_20260318` and later also accept [`response_inclusion`](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-search-tool#response-inclusion). ### Max uses The `max_uses` parameter limits the number of searches performed. If Claude attempts more searches than allowed, the `web_search_tool_result` is an error with the `max_uses_exceeded` error code. Simple factual queries typically use 1–3 searches; comparative or multientity research can use 10 or more. For guidance on choosing a value, see [Server tools](https://platform.claude.com/docs/en/agents-and-tools/tool-use/server-tools). ### Domain filtering Provide `allowed_domains` or `blocked_domains`, not both. If a request includes both, the API returns a 400 error. Entries are bare domains with an optional path, for example `example.com` or `example.com/blog`, without a scheme. For the full domain filtering rules, see [Domain filtering](https://platform.claude.com/docs/en/agents-and-tools/tool-use/server-tools#domain-filtering) in the Server tools guide. ### Localization The `user_location` parameter allows you to localize search results based on a user's location. Provide at least one of `city`, `region`, `country`, or `timezone`. * `type`: The type of location (must be `approximate`) * `city`: The city name * `region`: The region or state * `country`: The two-letter [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) country code. The API rejects unsupported country codes with a 400 error. * `timezone`: The [IANA timezone ID](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). ### Response inclusion Requires `web_search_20260318` or later. The `response_inclusion` parameter controls how search result blocks appear in the API response when the result was consumed by a completed [code execution](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool) call in the same turn. Set `"response_inclusion": "excluded"` to drop those nested `server_tool_use` and result block pairs entirely from the response, reducing output token costs for agentic workflows that don't need to echo raw search content back to the client. The default is `"full"`. Results from direct calls, or from code execution calls that paused before completing, are always returned in full so they can be sent back on the next turn. ```json JSON { "tools": [ { "type": "web_search_20260318", "name": "web_search", "response_inclusion": "excluded" } ] } ``` ## Response Here's an example response structure: ```json Output { "role": "assistant", "content": [ // 1. Claude's decision to search { "type": "text", "text": "I'll search for when Claude Shannon was born." }, // 2. The search query used { "type": "server_tool_use", "id": "srvtoolu_01WYG3ziw53XMcoyKL4XcZmE", "name": "web_search", "input": { "query": "claude shannon birth date" } }, // 3. Search results { "type": "web_search_tool_result", "tool_use_id": "srvtoolu_01WYG3ziw53XMcoyKL4XcZmE", "content": [ { "type": "web_search_result", "url": "https://en.wikipedia.org/wiki/Claude_Shannon", "title": "Claude Shannon - Wikipedia", "encrypted_content": "EqgfCioIARgBIiQ3YTAwMjY1Mi1mZjM5LTQ1NGUtODgxNC1kNjNjNTk1ZWI3Y...", "page_age": "April 30, 2025" } ] }, { "text": "Based on the search results, ", "type": "text" }, // 4. Claude's response with citations { "text": "Claude Shannon was born on April 30, 1916, in Petoskey, Michigan", "type": "text", "citations": [ { "type": "web_search_result_location", "url": "https://en.wikipedia.org/wiki/Claude_Shannon", "title": "Claude Shannon - Wikipedia", "encrypted_index": "Eo8BCioIAhgBIiQyYjQ0OWJmZi1lNm..", "cited_text": "Claude Elwood Shannon (April 30, 1916 – February 24, 2001) was an American mathematician, electrical engineer, computer scientist, cryptographer and i..." } ] } ], "id": "msg_a930390d3a", "usage": { "input_tokens": 6039, "output_tokens": 931, "server_tool_use": { "web_search_requests": 1 } }, "stop_reason": "end_turn" } ``` This example shows a direct search. When a search runs through [dynamic filtering](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-search-tool#dynamic-filtering), the response also contains the [code execution tool's](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool) result blocks, and each nested `server_tool_use` and `web_search_tool_result` pair carries a `caller` field identifying the code execution call that made it. ### Search results Search results include: * `url`: The URL of the source page * `title`: The title of the source page * `page_age`: When the site was last updated * `encrypted_content`: Encrypted content that you must pass back in multi-turn conversations To continue a conversation that contains search results, send the assistant's content blocks back exactly as you received them, including each result's `encrypted_content`. The API decrypts that content on later turns to restore the search results in Claude's context. If `encrypted_content` is missing or modified, the request fails with a 400 validation error. ### Citations Citations are always enabled for web search, and each `web_search_result_location` includes: * `url`: The URL of the cited source * `title`: The title of the cited source * `encrypted_index`: A reference that must be passed back for multi-turn conversations * `cited_text`: Up to 150 characters of the cited content The web search citation fields `cited_text`, `title`, and `url` do not count toward input or output token usage. When displaying API outputs directly to end users, citations must be included to the original source. If you are making modifications to API outputs, including by reprocessing or combining them with your own material before displaying them to end users, display citations as appropriate based on consultation with your legal team. ### Errors When the web search tool encounters an error (such as hitting rate limits), the Claude API still returns a 200 (success) response. The error is represented within the response body using the following structure: ```json Output { "type": "web_search_tool_result", "tool_use_id": "srvtoolu_a93jad", "content": { "type": "web_search_tool_result_error", "error_code": "max_uses_exceeded" } } ``` On an error, `content` is a single error object rather than a list of result blocks. A search that succeeds but matches no results returns an empty `content` list, not an error. These are the possible error codes: * `too_many_requests`: Rate limit exceeded * `invalid_tool_input`: Invalid search query parameter * `max_uses_exceeded`: Maximum web search tool uses exceeded * `query_too_long`: Query exceeds maximum length * `request_too_large`: The search request is too large, typically because of a long domain filter list * `unavailable`: An internal error occurred ### `pause_turn` stop reason The API can pause a long-running search turn and return `stop_reason: "pause_turn"`. To continue, send the paused assistant message back unchanged in a new request. If Claude calls web search and one of your client tools in the same group of parallel tool calls, the API returns `stop_reason: "tool_use"` instead and does not run the search yet. To continue, return the client tool results, and the API runs the search in the next request. See [Mixing server tools and client tools in one turn](https://platform.claude.com/docs/en/agents-and-tools/tool-use/server-tools#mixing-server-tools-and-client-tools-in-one-turn). For the server-side loop and `pause_turn` handling, see [The server-side loop and pause\_turn](https://platform.claude.com/docs/en/agents-and-tools/tool-use/server-tools#the-server-side-loop-and-pause-turn) in the Server tools guide. ## Prompt caching For caching tool definitions across turns, see [Tool use with prompt caching](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-use-with-prompt-caching). ## Streaming With streaming enabled, you'll receive search events as part of the stream. There will be a pause while the search runs: ```sse Output event: message_start data: {"type": "message_start", "message": {"id": "msg_abc123", "type": "message"}} event: content_block_start data: {"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}} // Claude's decision to search event: content_block_start data: {"type": "content_block_start", "index": 1, "content_block": {"type": "server_tool_use", "id": "srvtoolu_xyz789", "name": "web_search"}} // Search query streamed event: content_block_delta data: {"type": "content_block_delta", "index": 1, "delta": {"type": "input_json_delta", "partial_json": "{\"query\":\"latest quantum computing breakthroughs 2025\"}"}} // Pause while search executes // Search results streamed event: content_block_start data: {"type": "content_block_start", "index": 2, "content_block": {"type": "web_search_tool_result", "tool_use_id": "srvtoolu_xyz789", "content": [{"type": "web_search_result", "title": "Quantum Computing Breakthroughs in 2025", "url": "https://example.com"}]}} // Claude's response with citations (omitted in this example) ``` ## Batch requests You can include the web search tool in the [Messages Batches API](https://platform.claude.com/docs/en/build-with-claude/batch-processing). Web search tool calls through the Messages Batches API are priced the same as those in regular Messages API requests. To protect shared capacity, the Batches API throttles web search requests per organization, so large batches with many searches might take longer to complete. You can see your organization's web search rate limit on the [Rate limits](https://platform.claude.com/settings/limits) page in the Claude Console. To request a higher limit, contact sales from that page. ## Usage and pricing Web search usage is charged in addition to token usage: ```json { "usage": { "input_tokens": 105, "output_tokens": 6039, "cache_read_input_tokens": 7123, "cache_creation_input_tokens": 7345, "server_tool_use": { "web_search_requests": 1 } } } ``` Web search is available on the Claude API for **$10 per 1,000 searches**, plus standard token costs for search-generated content. Web search results retrieved throughout a conversation are counted as input tokens, in search iterations executed during a single turn and in subsequent conversation turns. Each web search counts as one use, regardless of the number of results returned. If an error occurs during web search, the web search will not be billed. ## Next steps Fetch and read content from specific URLs to augment Claude's context with live web content. Work with Anthropic-executed tools: server\_tool\_use blocks, pause\_turn continuation, and domain filtering. Directory of Anthropic-provided tools and reference for optional tool definition properties. ### Tool infrastructure --- title: Fine-grained tool streaming url: https://platform.claude.com/docs/en/agents-and-tools/tool-use/fine-grained-tool-streaming description: Stream tool inputs without server-side JSON buffering for latency-sensitive applications. --- For how zero data retention (ZDR) applies to this feature, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention). Fine-grained tool streaming delivers a tool's input to your client as Claude generates it, without server-side buffering or JSON validation. Skipping the buffering step reduces the time to the first fragment of a large parameter, such as a document or a block of code, and the fragments arrive through the same [Streaming messages](https://platform.claude.com/docs/en/build-with-claude/streaming) events as standard tool use. Because the API does not buffer or validate a tool's input before streaming it, you might receive partial or invalid JSON. A response that ends with the [stop reason](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons) `max_tokens` can also cut a parameter off midway. Accumulate the fragments, guard the parse, and see [Handling invalid JSON in tool responses](https://platform.claude.com/docs/en/agents-and-tools/tool-use/fine-grained-tool-streaming#handling-invalid-json-in-tool-responses) for how to return unparseable input to Claude. ## How to use fine-grained tool streaming All models support fine-grained tool streaming on the Claude API, [Amazon Bedrock](https://platform.claude.com/docs/en/build-with-claude/claude-in-amazon-bedrock), [Claude Platform on AWS](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws), [Google Cloud](https://platform.claude.com/docs/en/build-with-claude/claude-on-vertex-ai), and [Microsoft Foundry](https://platform.claude.com/docs/en/build-with-claude/claude-in-microsoft-foundry). To use it, set `eager_input_streaming` to `true` on any user-defined tool where you want fine-grained streaming enabled, and enable streaming on your request. The `eager_input_streaming` field is optional. Setting it to `true` turns on fine-grained streaming for that tool, and omitting it gives you standard buffered streaming, in which the API buffers and validates each parameter value before streaming it back. The exception is a request that still sends the legacy `fine-grained-tool-streaming-2025-05-14` beta header, which turns fine-grained streaming on for tools that leave the field unset. The per-tool field replaces that header, and an explicit `false` keeps buffered streaming for a tool even when a request still sends it. See [Tool reference](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-reference) for the field definition. The following example turns on fine-grained streaming for a `make_file` tool and asks Claude for a long poem, so the tool input is large enough to watch it stream in: ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-opus-5", "max_tokens": 65536, "tools": [ { "name": "make_file", "description": "Write text to a file", "eager_input_streaming": true, "input_schema": { "type": "object", "properties": { "filename": { "type": "string", "description": "The filename to write text to" }, "lines_of_text": { "type": "array", "description": "An array of lines of text to write to the file" } }, "required": ["filename", "lines_of_text"] } } ], "messages": [ { "role": "user", "content": "Can you write a long poem and make a file called poem.txt?" } ], "stream": true }' ``` ```bash CLI ant messages create --stream --format jsonl <<'YAML' | model: claude-opus-5 max_tokens: 65536 tools: - name: make_file description: Write text to a file eager_input_streaming: true input_schema: type: object properties: filename: type: string description: The filename to write text to lines_of_text: type: array description: An array of lines of text to write to the file required: - filename - lines_of_text messages: - role: user content: Can you write a long poem and make a file called poem.txt? YAML jq -rj 'select(.delta.type == "input_json_delta") | .delta.partial_json' ``` ```python Python client = anthropic.Anthropic() with client.messages.stream( max_tokens=65536, model="claude-opus-5", tools=[ { "name": "make_file", "description": "Write text to a file", "eager_input_streaming": True, "input_schema": { "type": "object", "properties": { "filename": { "type": "string", "description": "The filename to write text to", }, "lines_of_text": { "type": "array", "description": "An array of lines of text to write to the file", }, }, "required": ["filename", "lines_of_text"], }, } ], messages=[ { "role": "user", "content": "Can you write a long poem and make a file called poem.txt?", } ], ) as stream: for event in stream: if event.type == "input_json": print(event.partial_json, end="", flush=True) final_message = stream.get_final_message() print() for block in final_message.content: if block.type == "tool_use": print(f"Complete tool input: {block.input}") ``` ```typescript TypeScript const client = new Anthropic(); const stream = client.messages.stream({ model: "claude-opus-5", max_tokens: 65536, tools: [ { name: "make_file", description: "Write text to a file", eager_input_streaming: true, input_schema: { type: "object", properties: { filename: { type: "string", description: "The filename to write text to" }, lines_of_text: { type: "array", description: "An array of lines of text to write to the file" } }, required: ["filename", "lines_of_text"] } } ], messages: [ { role: "user", content: "Can you write a long poem and make a file called poem.txt?" } ] }); stream.on("inputJson", (partialJson) => { process.stdout.write(partialJson); }); const message = await stream.finalMessage(); console.log(); for (const block of message.content) { if (block.type === "tool_use") { console.log("Complete tool input:", block.input); } } ``` ```csharp C# AnthropicClient client = new(); MessageCreateParams parameters = new() { Model = Model.ClaudeOpus5, MaxTokens = 65536, Tools = [ new Tool { Name = "make_file", Description = "Write text to a file", EagerInputStreaming = true, InputSchema = new InputSchema { Properties = new Dictionary { ["filename"] = JsonSerializer.SerializeToElement( new { type = "string", description = "The filename to write text to" } ), ["lines_of_text"] = JsonSerializer.SerializeToElement( new { type = "array", description = "An array of lines of text to write to the file" } ), }, Required = ["filename", "lines_of_text"], }, }, ], Messages = [ new() { Role = Role.User, Content = "Can you write a long poem and make a file called poem.txt?", }, ], }; // The C# example assembles the input itself: content block index -> accumulated JSON var toolInputs = new Dictionary(); await foreach (var streamEvent in client.Messages.CreateStreaming(parameters)) { if ( streamEvent.TryPickContentBlockStart(out var start) && start.ContentBlock.TryPickToolUse(out _) ) { toolInputs[start.Index] = new StringBuilder(); } else if ( streamEvent.TryPickContentBlockDelta(out var delta) && delta.Delta.TryPickInputJson(out var inputJson) ) { Console.Write(inputJson.PartialJson); toolInputs[delta.Index].Append(inputJson.PartialJson); } } Console.WriteLine(); foreach (var accumulatedInput in toolInputs.Values) { Console.WriteLine($"Complete tool input: {accumulatedInput}"); } ``` ```go Go client := anthropic.NewClient() makeFileTool := anthropic.ToolParam{ Name: "make_file", Description: anthropic.String("Write text to a file"), EagerInputStreaming: anthropic.Bool(true), InputSchema: anthropic.ToolInputSchemaParam{ Properties: map[string]any{ "filename": map[string]any{ "type": "string", "description": "The filename to write text to", }, "lines_of_text": map[string]any{ "type": "array", "description": "An array of lines of text to write to the file", }, }, Required: []string{"filename", "lines_of_text"}, }, } stream := client.Messages.NewStreaming(context.Background(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 65536, Tools: []anthropic.ToolUnionParam{{OfTool: &makeFileTool}}, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock( "Can you write a long poem and make a file called poem.txt?", )), }, }) message := anthropic.Message{} for stream.Next() { event := stream.Current() if err := message.Accumulate(event); err != nil { panic(err) } if delta, ok := event.AsAny().(anthropic.ContentBlockDeltaEvent); ok { if inputJSON, ok := delta.Delta.AsAny().(anthropic.InputJSONDelta); ok { fmt.Print(inputJSON.PartialJSON) } } } if err := stream.Err(); err != nil { panic(err) } fmt.Println() for _, block := range message.Content { if toolUse, ok := block.AsAny().(anthropic.ToolUseBlock); ok { fmt.Printf("Complete tool input: %s\n", toolUse.Input) } } ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); Tool makeFileTool = Tool.builder() .name("make_file") .description("Write text to a file") .eagerInputStreaming(true) .inputSchema(Tool.InputSchema.builder() .properties(Tool.InputSchema.Properties.builder() .putAdditionalProperty("filename", JsonValue.from(Map.of( "type", "string", "description", "The filename to write text to"))) .putAdditionalProperty("lines_of_text", JsonValue.from(Map.of( "type", "array", "description", "An array of lines of text to write to the file"))) .build()) .addRequired("filename") .addRequired("lines_of_text") .build()) .build(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(65536L) .addTool(makeFileTool) .addUserMessage("Can you write a long poem and make a file called poem.txt?") .build(); MessageAccumulator accumulator = MessageAccumulator.create(); try (StreamResponse streamResponse = client.messages().createStreaming(params)) { streamResponse.stream().forEach(event -> { accumulator.accumulate(event); if (event.isContentBlockDelta()) { var delta = event.asContentBlockDelta().delta(); if (delta.isInputJson()) { IO.print(delta.asInputJson().partialJson()); } } }); } IO.println(""); accumulator.message().content().forEach(block -> block.toolUse().ifPresent(toolUse -> IO.println("Complete tool input: " + toolUse._input()))); ``` ```php PHP use Anthropic\Client; use Anthropic\Messages\InputJSONDelta; use Anthropic\Messages\Model; use Anthropic\Messages\RawContentBlockDeltaEvent; use Anthropic\Messages\RawContentBlockStartEvent; use Anthropic\Messages\ToolUseBlock; $client = new Client(); $stream = $client->messages->createStream( maxTokens: 65536, model: Model::CLAUDE_OPUS_5, tools: [ [ 'name' => 'make_file', 'description' => 'Write text to a file', 'eager_input_streaming' => true, 'input_schema' => [ 'type' => 'object', 'properties' => [ 'filename' => [ 'type' => 'string', 'description' => 'The filename to write text to', ], 'lines_of_text' => [ 'type' => 'array', 'description' => 'An array of lines of text to write to the file', ], ], 'required' => ['filename', 'lines_of_text'], ], ], ], messages: [ [ 'role' => 'user', 'content' => 'Can you write a long poem and make a file called poem.txt?', ], ], ); // The PHP example assembles the input itself: index => accumulated JSON string $toolInputs = []; foreach ($stream as $event) { if ( $event instanceof RawContentBlockStartEvent && $event->contentBlock instanceof ToolUseBlock ) { $toolInputs[$event->index] = ''; } elseif ( $event instanceof RawContentBlockDeltaEvent && $event->delta instanceof InputJSONDelta ) { echo $event->delta->partialJSON; $toolInputs[$event->index] .= $event->delta->partialJSON; } } echo "\n"; foreach ($toolInputs as $toolInput) { echo "Complete tool input: {$toolInput}\n"; } ``` ```ruby Ruby client = Anthropic::Client.new stream = client.messages.stream( model: Anthropic::Models::Model::CLAUDE_OPUS_5, max_tokens: 65_536, tools: [ { name: "make_file", description: "Write text to a file", eager_input_streaming: true, input_schema: { type: "object", properties: { filename: { type: "string", description: "The filename to write text to" }, lines_of_text: { type: "array", description: "An array of lines of text to write to the file" } }, required: ["filename", "lines_of_text"] } } ], messages: [ { role: "user", content: "Can you write a long poem and make a file called poem.txt?" } ] ) stream.each do |event| print event.partial_json if event.is_a?(Anthropic::Streaming::InputJsonEvent) end puts stream.accumulated_message.content.each do |block| puts "Complete tool input: #{block.input}" if block.type == :tool_use end ``` Every tab turns on fine-grained streaming for the `make_file` tool. The SDK tabs print each input fragment the moment it arrives, then print the complete accumulated input once the stream ends. The cURL tab shows the raw event stream, and the CLI tab uses `jq` to print just the fragments. Because the printed fragments join into the full tool input, the poem fills your terminal as Claude writes it: ```text wrap {"filename": "poem.txt", "lines_of_text": ["The Wanderer's Journey", "", "I.", "", "Beneath the vast and star-strewn sky,", "Where silver moonbeams softly lie,", ... Complete tool input: {"filename": "poem.txt", "lines_of_text": ["The Wanderer's Journey", ...]} ``` Without `eager_input_streaming`, the API buffers and validates each parameter value before streaming it back, so nothing prints for a large parameter until Claude has finished generating it. With it, fragments start arriving as soon as Claude begins the parameter, and they are typically longer, with fewer mid-word breaks. ## Accumulating tool input deltas The accumulation contract is the same as for standard tool-use streaming, so this section applies with and without `eager_input_streaming`. See [Input JSON delta](https://platform.claude.com/docs/en/build-with-claude/streaming#input-json-delta) in Streaming messages for the event format. Fine-grained tool streaming changes what you can assume about the result: the server streams fragments without validating them, so the accumulated string might not be valid JSON. When a `tool_use` content block streams, the initial `content_block_start` event contains `input: {}` (an empty object). This is a placeholder. The actual input arrives as a series of `input_json_delta` events, each carrying a `partial_json` string fragment. To assemble the full input, concatenate these fragments and parse the result when the block closes. Where your SDK provides an accumulator helper (as the Python, TypeScript, Go, Java, and Ruby tabs in the previous example do), it handles this for you. The manual pattern is for SDKs without a helper, or when you want full control over how the input is assembled. The accumulation contract: 1. On `content_block_start` with `type: "tool_use"`, initialize an empty string: `input_json = ""` 2. For each `content_block_delta` with `type: "input_json_delta"`, append: `input_json += event.delta.partial_json` 3. On `content_block_stop`, parse the accumulated string Guard the parse, as the following SDK examples do. A response can also stop at `max_tokens` midway through a parameter. Check the [stop reason](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons) and decide whether to retry the request with a higher `max_tokens` or repair the partial input. The type mismatch between the initial `input: {}` (object) and `partial_json` (string) is by design. The empty object marks the slot in the content array. The delta strings build the real value. ```bash cURL # Accumulating per-block input deltas needs a programming language; the first # example's CLI tab shows the raw fragments with jq. See the SDK tabs. ``` ```bash CLI # Accumulating per-block input deltas needs a programming language; the first # example's CLI tab shows the raw fragments with jq. See the SDK tabs. ``` ```python Python client = anthropic.Anthropic() tool_inputs: dict[int, str] = {} # index -> accumulated JSON string with client.messages.stream( model="claude-opus-5", max_tokens=1024, tools=[ { "name": "get_weather", "description": "Get current weather for a city", "eager_input_streaming": True, "input_schema": { "type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"], }, } ], messages=[{"role": "user", "content": "Weather in Paris?"}], ) as stream: for event in stream: match event.type: case "content_block_start" if event.content_block.type == "tool_use": tool_inputs[event.index] = "" case "content_block_delta" if event.delta.type == "input_json_delta": tool_inputs[event.index] += event.delta.partial_json case "content_block_stop" if event.index in tool_inputs: raw_input = tool_inputs[event.index] try: parsed = json.loads(raw_input) except json.JSONDecodeError: # The accumulated string is not guaranteed to be valid JSON. # See "Handling invalid JSON in tool responses" on this page. print(f"Invalid tool input: {raw_input}") else: print(f"Tool input: {parsed}") ``` ```typescript TypeScript const client = new Anthropic(); const toolInputs = new Map(); const stream = client.messages.stream({ model: "claude-opus-5", max_tokens: 1024, tools: [ { name: "get_weather", description: "Get current weather for a city", eager_input_streaming: true, input_schema: { type: "object", properties: { city: { type: "string" } }, required: ["city"] } } ], messages: [{ role: "user", content: "Weather in Paris?" }] }); for await (const event of stream) { if (event.type === "content_block_start" && event.content_block.type === "tool_use") { toolInputs.set(event.index, ""); } else if (event.type === "content_block_delta" && event.delta.type === "input_json_delta") { toolInputs.set( event.index, (toolInputs.get(event.index) ?? "") + event.delta.partial_json ); } else if (event.type === "content_block_stop" && toolInputs.has(event.index)) { const rawInput = toolInputs.get(event.index)!; try { console.log("Tool input:", JSON.parse(rawInput)); } catch { // The accumulated string is not guaranteed to be valid JSON. // See "Handling invalid JSON in tool responses" on this page. console.log("Invalid tool input:", rawInput); } } } ``` ```csharp C# AnthropicClient client = new(); MessageCreateParams parameters = new() { Model = Model.ClaudeOpus5, MaxTokens = 1024, Tools = [ new Tool { Name = "get_weather", Description = "Get current weather for a city", EagerInputStreaming = true, InputSchema = new InputSchema { Properties = new Dictionary { ["city"] = JsonSerializer.SerializeToElement(new { type = "string" }), }, Required = ["city"], }, }, ], Messages = [new() { Role = Role.User, Content = "Weather in Paris?" }], }; // Block index -> accumulated JSON fragments // This example accumulates the deltas manually to show the raw stream; // the SDK's MessageContentAggregator can also accumulate tool input automatically. var toolInputs = new Dictionary(); await foreach (var streamEvent in client.Messages.CreateStreaming(parameters)) { if ( streamEvent.TryPickContentBlockStart(out var start) && start.ContentBlock.TryPickToolUse(out _) ) { toolInputs[start.Index] = new StringBuilder(); } else if ( streamEvent.TryPickContentBlockDelta(out var delta) && delta.Delta.TryPickInputJson(out var inputJson) ) { toolInputs[delta.Index].Append(inputJson.PartialJson); } else if ( streamEvent.TryPickContentBlockStop(out var stop) && toolInputs.TryGetValue(stop.Index, out var accumulated) ) { try { using var parsed = JsonDocument.Parse(accumulated.ToString()); Console.WriteLine($"Tool input: {parsed.RootElement}"); } catch (JsonException) { // The accumulated string is not guaranteed to be valid JSON. // See "Handling invalid JSON in tool responses" on this page. Console.WriteLine($"Invalid tool input: {accumulated}"); } } } ``` ```go Go client := anthropic.NewClient() toolInputs := map[int64]string{} // content block index -> accumulated JSON stream := client.Messages.NewStreaming(context.Background(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Tools: []anthropic.ToolUnionParam{{ OfTool: &anthropic.ToolParam{ Name: "get_weather", Description: anthropic.String("Get current weather for a city"), EagerInputStreaming: anthropic.Bool(true), InputSchema: anthropic.ToolInputSchemaParam{ Properties: map[string]any{ "city": map[string]any{"type": "string"}, }, Required: []string{"city"}, }, }, }}, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Weather in Paris?")), }, }) for stream.Next() { switch event := stream.Current().AsAny().(type) { case anthropic.ContentBlockStartEvent: if _, ok := event.ContentBlock.AsAny().(anthropic.ToolUseBlock); ok { toolInputs[event.Index] = "" } case anthropic.ContentBlockDeltaEvent: if delta, ok := event.Delta.AsAny().(anthropic.InputJSONDelta); ok { toolInputs[event.Index] += delta.PartialJSON } case anthropic.ContentBlockStopEvent: if accumulated, ok := toolInputs[event.Index]; ok { var parsed map[string]any if err := json.Unmarshal([]byte(accumulated), &parsed); err != nil { // The accumulated string is not guaranteed to be valid JSON. // See "Handling invalid JSON in tool responses" on this page. fmt.Println("Invalid tool input:", accumulated) } else { fmt.Println("Tool input:", parsed) } } } } if err := stream.Err(); err != nil { panic(err) } ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); ObjectMapper objectMapper = new ObjectMapper(); Tool weatherTool = Tool.builder() .name("get_weather") .description("Get current weather for a city") .eagerInputStreaming(true) .inputSchema(Tool.InputSchema.builder() .properties(Tool.InputSchema.Properties.builder() .putAdditionalProperty("city", JsonValue.from(Map.of("type", "string"))) .build()) .addRequired("city") .build()) .build(); MessageCreateParams createParams = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .addTool(weatherTool) .addUserMessage("Weather in Paris?") .build(); // Content block index -> accumulated tool input JSON Map toolInputs = new HashMap<>(); try (StreamResponse streamResponse = client.messages().createStreaming(createParams)) { var eventIterator = streamResponse.stream().iterator(); while (eventIterator.hasNext()) { RawMessageStreamEvent event = eventIterator.next(); if (event.isContentBlockStart()) { var blockStart = event.asContentBlockStart(); if (blockStart.contentBlock().isToolUse()) { toolInputs.put(blockStart.index(), new StringBuilder()); } } else if (event.isContentBlockDelta()) { var blockDelta = event.asContentBlockDelta(); if (blockDelta.delta().isInputJson() && toolInputs.containsKey(blockDelta.index())) { toolInputs.get(blockDelta.index()).append(blockDelta.delta().asInputJson().partialJson()); } } else if (event.isContentBlockStop()) { var blockStop = event.asContentBlockStop(); if (toolInputs.containsKey(blockStop.index())) { String accumulated = toolInputs.get(blockStop.index()).toString(); try { IO.println("Tool input: " + objectMapper.readTree(accumulated)); } catch (JsonProcessingException e) { // The accumulated string is not guaranteed to be valid JSON. // See "Handling invalid JSON in tool responses" on this page. IO.println("Invalid tool input: " + accumulated); } } } } } ``` ```php PHP use Anthropic\Client; use Anthropic\Messages\InputJSONDelta; use Anthropic\Messages\Model; use Anthropic\Messages\RawContentBlockDeltaEvent; use Anthropic\Messages\RawContentBlockStartEvent; use Anthropic\Messages\RawContentBlockStopEvent; use Anthropic\Messages\ToolUseBlock; $client = new Client(); // The PHP SDK does not provide a stream accumulator for tool input; // the manual pattern shown here is the supported approach. $toolInputs = []; // index => accumulated JSON string $stream = $client->messages->createStream( maxTokens: 1024, model: Model::CLAUDE_OPUS_5, tools: [ [ 'name' => 'get_weather', 'description' => 'Get current weather for a city', 'eager_input_streaming' => true, 'input_schema' => [ 'type' => 'object', 'properties' => ['city' => ['type' => 'string']], 'required' => ['city'], ], ], ], messages: [['role' => 'user', 'content' => 'Weather in Paris?']], ); foreach ($stream as $event) { if ( $event instanceof RawContentBlockStartEvent && $event->contentBlock instanceof ToolUseBlock ) { $toolInputs[$event->index] = ''; } elseif ( $event instanceof RawContentBlockDeltaEvent && $event->delta instanceof InputJSONDelta ) { $toolInputs[$event->index] .= $event->delta->partialJSON; } elseif ( $event instanceof RawContentBlockStopEvent && isset($toolInputs[$event->index]) ) { $accumulated = $toolInputs[$event->index]; try { $parsed = json_decode($accumulated, associative: true, flags: JSON_THROW_ON_ERROR); echo "Tool input: " . json_encode($parsed) . "\n"; } catch (JsonException $e) { // The accumulated string is not guaranteed to be valid JSON. // See "Handling invalid JSON in tool responses" on this page. echo "Invalid tool input: {$accumulated}\n"; } } } ``` ```ruby Ruby client = Anthropic::Client.new tool_inputs = {} # index -> accumulated JSON string stream = client.messages.stream_raw( model: Anthropic::Models::Model::CLAUDE_OPUS_5, max_tokens: 1024, tools: [ { name: "get_weather", description: "Get current weather for a city", eager_input_streaming: true, input_schema: { type: "object", properties: {city: {type: "string"}}, required: ["city"] } } ], messages: [{role: "user", content: "Weather in Paris?"}] ) stream.each do |event| case event when Anthropic::Models::RawContentBlockStartEvent tool_inputs[event.index] = +"" if event.content_block.type == :tool_use when Anthropic::Models::RawContentBlockDeltaEvent if event.delta.is_a?(Anthropic::Models::InputJSONDelta) tool_inputs[event.index] << event.delta.partial_json end when Anthropic::Models::RawContentBlockStopEvent if tool_inputs.key?(event.index) accumulated = tool_inputs[event.index] begin parsed = JSON.parse(accumulated) puts "Tool input: #{parsed}" rescue JSON::ParserError # The accumulated string is not guaranteed to be valid JSON. # See "Handling invalid JSON in tool responses" on this page. puts "Invalid tool input: #{accumulated}" end end end end ``` Reacting to fragments and assembling them are separate concerns. The first example reacts to each fragment as it arrives and still hands assembly to the SDK in the tabs that use an accumulator helper. Use the manual pattern when you are not using an accumulator helper or when you want full control over assembly. ## Handling invalid JSON in tool responses With fine-grained tool streaming, the accumulated input for a tool call might be invalid or incomplete JSON. When it is, you cannot run the tool, so report the failure back to Claude instead. The `content` of a tool result does not have to be JSON, but wrapping the raw string in a JSON object under a single key makes it unambiguous to Claude that you received invalid JSON, and preserves the original input for debugging: ```json { "INVALID_JSON": "" } ``` Return the wrapper, serialized to a string, as the `content` of a [tool result](https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls#handling-errors-with-is-error) content block with `is_error` set to `true`: ```json { "type": "tool_result", "tool_use_id": "toolu_01A09q90qw90lq917835lq9", "is_error": true, "content": "{\"INVALID_JSON\": \"\"}" } ``` Build the wrapper with your JSON library rather than by concatenating strings, so quotes and other special characters in the invalid input are escaped correctly. ## Next steps Understand how the context window works, how extended thinking and tool use count toward it, and how to manage context as conversations grow. Stream Messages API responses incrementally with server-sent events, including text, tool use, and extended thinking deltas. Parse tool\_use blocks, format tool\_result responses, and handle errors with is\_error. Directory of Anthropic-provided tools and reference for optional tool definition properties. --- title: Manage tool context url: https://platform.claude.com/docs/en/agents-and-tools/tool-use/manage-tool-context description: Choose between tool search, programmatic tool calling, prompt caching, and context editing to manage context bloat. --- Tool definitions and accumulated `tool_result` blocks consume your context window. Long-running agents with many tools or many turns can exhaust available context before the task is finished. Four approaches address this at different points in the pipeline. ## The four approaches Each approach targets a different source of context pressure. Pick the one that matches where your tokens are going. | Approach | What it reduces | When it fits | Learn more | | ------------------------- | --------------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | Tool search | Tool definitions loaded upfront | Large toolsets (20+ tools) where most tools aren't needed every turn | [Tool search tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool) | | Programmatic tool calling | `tool_result` roundtrips | Chains of tool calls that can execute as a single script | [Programmatic tool calling](https://platform.claude.com/docs/en/agents-and-tools/tool-use/programmatic-tool-calling) | | Prompt caching | Token cost of repeated tool definitions | Stable toolsets across many requests | [Tool use with prompt caching](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-use-with-prompt-caching) | | Context editing | Old `tool_result` blocks in history | Long conversations where early results are no longer relevant | [Context editing](https://platform.claude.com/docs/en/build-with-claude/context-editing) | ### Tool search Tool search keeps tool definitions out of the context window until Claude asks for them. Instead of sending 50 tool schemas upfront, you send a single `tool_search` tool and let Claude discover the rest on demand. This trades a small amount of latency (one extra turn to look up a tool) for a large reduction in baseline context usage. ### Programmatic tool calling Programmatic tool calling collapses a sequence of tool calls into a single code block that Claude writes and Anthropic's code execution sandbox runs. Rather than five roundtrips of `tool_use` and `tool_result`, Claude emits one script that calls all five functions from within the sandbox. The intermediate results never enter the conversation history. ### Prompt caching Prompt caching doesn't reduce the number of tokens in context, but it reduces what you pay for them on subsequent requests. If your tool definitions are stable, cache them once and reuse the cached prefix across thousands of requests. This is the right choice when the toolset is large but fixed. ### Context editing Context editing removes old `tool_result` blocks from the conversation history once they've served their purpose. A long agent loop might produce hundreds of intermediate results that were useful at the time but are now dead weight. Context editing lets you trim them without restarting the conversation. ## Combining approaches These approaches compose. A long-running agent might use tool search to keep the toolset lean, prompt caching to amortize the cost of the remaining definitions, and context editing to trim stale results as the conversation grows. Each solves a different part of the problem, so there's no conflict in using them together. A reasonable starting point for a high-volume agent: 1. Enable prompt caching on your tool definitions from day one. Cache writes carry a 25% markup over base input pricing, which pays back on the second request that hits the cache. 2. Add tool search once your toolset grows past roughly 20 tools or your baseline context usage becomes noticeable. 3. Add context editing once individual conversations start running long enough that early results become irrelevant. 4. Consider programmatic tool calling if you notice repetitive chains of small tool calls that could run as a single batch. ## Next steps Load tool definitions on demand instead of upfront. Collapse tool-call chains into a single executable script. Cache tool definitions across requests to cut token costs. Trim stale tool results from long-running conversations. --- title: Programmatic tool calling url: https://platform.claude.com/docs/en/agents-and-tools/tool-use/programmatic-tool-calling description: Let Claude call your tools from code in the code execution container, cutting model round trips and token use in multi-tool workflows. --- Programmatic tool calling allows Claude to write code that calls your tools programmatically within a [code execution](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool) container, rather than requiring round trips through the model for each tool invocation. This reduces latency for multi-tool workflows and decreases token consumption by allowing Claude to filter or process data before it reaches the model's context window. On agentic search benchmarks like [BrowseComp](https://arxiv.org/abs/2504.12516) and [DeepSearchQA](https://github.com/google-deepmind/deepsearchqa), which test multistep web research and complex information retrieval, adding programmatic tool calling on top of basic search tools improved performance by an average of 11% while using 24% fewer input tokens (see [Improved web search with dynamic filtering](https://claude.com/blog/improved-web-search-with-dynamic-filtering)). Consider checking budget compliance across 20 employees: the traditional approach requires 20 separate model round-trips, pulling thousands of expense line items into the context along the way. With programmatic tool calling, a single script runs all 20 lookups, filters the results, and returns only the employees who exceeded their limits, shrinking what Claude needs to reason over from hundreds of kilobytes down to a handful of lines. For a deeper look at the inference and context costs that programmatic tool calling addresses, see [Advanced tool use](https://www.anthropic.com/engineering/advanced-tool-use). This feature requires the code execution tool to be enabled. For how zero data retention (ZDR) applies to this feature, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention). ## Model compatibility Programmatic tool calling requires `code_execution_20260120` or later, which is supported on the following models: | Model | | ---------------------------------------------- | | Claude Fable 5 (claude-fable-5) | | Claude Mythos 5 (claude-mythos-5) | | Claude Opus 5 (claude-opus-5) | | Claude Opus 4.8 (claude-opus-4-8) | | Claude Opus 4.7 (claude-opus-4-7) | | Claude Opus 4.6 (claude-opus-4-6) | | Claude Sonnet 5 (claude-sonnet-5) | | Claude Sonnet 4.6 (claude-sonnet-4-6) | | Claude Opus 4.5 (claude-opus-4-5-20251101) | | Claude Sonnet 4.5 (claude-sonnet-4-5-20250929) | For the full code execution tool version matrix, see the [code execution tool model compatibility table](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool#model-compatibility). Programmatic tool calling is available on the Claude API, [Claude Platform on AWS](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws), and [Microsoft Foundry](https://platform.claude.com/docs/en/build-with-claude/claude-in-microsoft-foundry). On Microsoft Foundry, programmatic tool calling requires a [Hosted on Anthropic deployment](https://platform.claude.com/docs/en/build-with-claude/claude-in-microsoft-foundry#additional-features-not-supported-when-hosted-on-azure). It is not currently available on Amazon Bedrock or Google Cloud. ## Quick start Here's an example where Claude programmatically queries a database multiple times and aggregates results. Adding `allowed_callers: ["code_execution_20260120"]` to a tool definition is what makes that tool callable from within code execution (see [The `allowed_callers` field](https://platform.claude.com/docs/en/agents-and-tools/tool-use/programmatic-tool-calling#the-allowed-callers-field)): ```bash cURL curl https://api.anthropic.com/v1/messages \ --header "x-api-key: $ANTHROPIC_API_KEY" \ --header "anthropic-version: 2023-06-01" \ --header "content-type: application/json" \ --data '{ "model": "claude-opus-5", "max_tokens": 4096, "messages": [ { "role": "user", "content": "Query sales data for the West, East, and Central regions, then tell me which region had the highest revenue" } ], "tools": [ { "type": "code_execution_20260120", "name": "code_execution" }, { "name": "query_database", "description": "Execute a SQL query against the sales database. Returns a list of rows as JSON objects.", "input_schema": { "type": "object", "properties": { "sql": { "type": "string", "description": "SQL query to execute" } }, "required": ["sql"] }, "allowed_callers": ["code_execution_20260120"] } ] }' ``` ```bash CLI ant messages create <<'YAML' model: claude-opus-5 max_tokens: 4096 messages: - role: user content: >- Query sales data for the West, East, and Central regions, then tell me which region had the highest revenue tools: - type: code_execution_20260120 name: code_execution - name: query_database description: >- Execute a SQL query against the sales database. Returns a list of rows as JSON objects. input_schema: type: object properties: sql: type: string description: SQL query to execute required: - sql allowed_callers: - code_execution_20260120 YAML ``` ```python Python client = anthropic.Anthropic() response = client.messages.create( model="claude-opus-5", max_tokens=4096, messages=[ { "role": "user", "content": "Query sales data for the West, East, and Central regions, then tell me which region had the highest revenue", } ], tools=[ {"type": "code_execution_20260120", "name": "code_execution"}, { "name": "query_database", "description": "Execute a SQL query against the sales database. Returns a list of rows as JSON objects.", "input_schema": { "type": "object", "properties": { "sql": {"type": "string", "description": "SQL query to execute"} }, "required": ["sql"], }, "allowed_callers": ["code_execution_20260120"], }, ], ) print(response) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 4096, messages: [ { role: "user", content: "Query sales data for the West, East, and Central regions, then tell me which region had the highest revenue" } ], tools: [ { type: "code_execution_20260120", name: "code_execution" }, { name: "query_database", description: "Execute a SQL query against the sales database. Returns a list of rows as JSON objects.", input_schema: { type: "object" as const, properties: { sql: { type: "string", description: "SQL query to execute" } }, required: ["sql"] }, allowed_callers: ["code_execution_20260120"] } ] }); console.log(response); ``` ```csharp C# AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 4096, Messages = [ new() { Role = Role.User, Content = "Query sales data for the West, East, and Central regions, then tell me which region had the highest revenue" } ], Tools = [ new CodeExecutionTool20260120(), new ToolUnion(new Tool() { Name = "query_database", Description = "Execute a SQL query against the sales database. Returns a list of rows as JSON objects.", InputSchema = new InputSchema() { Properties = new Dictionary { ["sql"] = JsonSerializer.SerializeToElement(new { type = "string", description = "SQL query to execute" }), }, Required = ["sql"], }, AllowedCallers = ["code_execution_20260120"] }), ] }; var message = await client.Messages.Create(parameters); Console.WriteLine(message); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 4096, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Query sales data for the West, East, and Central regions, then tell me which region had the highest revenue")), }, Tools: []anthropic.ToolUnionParam{ {OfCodeExecutionTool20260120: &anthropic.CodeExecutionTool20260120Param{}}, {OfTool: &anthropic.ToolParam{ Name: "query_database", Description: anthropic.String("Execute a SQL query against the sales database. Returns a list of rows as JSON objects."), InputSchema: anthropic.ToolInputSchemaParam{ Properties: map[string]any{ "sql": map[string]any{ "type": "string", "description": "SQL query to execute", }, }, Required: []string{"sql"}, }, AllowedCallers: []string{"code_execution_20260120"}, }}, }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java import com.anthropic.models.messages.CodeExecutionTool20260120; // ... void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(4096L) .addUserMessage("Query sales data for the West, East, and Central regions, then tell me which region had the highest revenue") .addTool(CodeExecutionTool20260120.builder().build()) .addTool(Tool.builder() .name("query_database") .description("Execute a SQL query against the sales database. Returns a list of rows as JSON objects.") .inputSchema(InputSchema.builder() .properties(JsonValue.from(Map.of( "sql", Map.of( "type", "string", "description", "SQL query to execute" ) ))) .putAdditionalProperty("required", JsonValue.from(List.of("sql"))) .build()) .allowedCallers(List.of(Tool.AllowedCaller.of("code_execution_20260120"))) .build()) .build(); Message response = client.messages().create(params); IO.println(response); } ``` ```php PHP $client = new Client(); $message = $client->messages->create( maxTokens: 4096, messages: [ ['role' => 'user', 'content' => 'Query sales data for the West, East, and Central regions, then tell me which region had the highest revenue'], ], model: 'claude-opus-5', tools: [ [ 'type' => 'code_execution_20260120', 'name' => 'code_execution', ], [ 'name' => 'query_database', 'description' => 'Execute a SQL query against the sales database. Returns a list of rows as JSON objects.', 'input_schema' => [ 'type' => 'object', 'properties' => [ 'sql' => [ 'type' => 'string', 'description' => 'SQL query to execute', ], ], 'required' => ['sql'], ], 'allowed_callers' => ['code_execution_20260120'], ], ], ); echo $message; ``` ```ruby Ruby client = Anthropic::Client.new message = client.messages.create( model: "claude-opus-5", max_tokens: 4096, messages: [ { role: "user", content: "Query sales data for the West, East, and Central regions, then tell me which region had the highest revenue" } ], tools: [ { type: "code_execution_20260120", name: "code_execution" }, { name: "query_database", description: "Execute a SQL query against the sales database. Returns a list of rows as JSON objects.", input_schema: { type: "object", properties: { sql: { type: "string", description: "SQL query to execute" } }, required: ["sql"] }, allowed_callers: ["code_execution_20260120"] } ] ) puts message ``` The response stops with `stop_reason: "tool_use"`, a `container` ID, and a `tool_use` block for `query_database` whose `caller` field identifies the code execution run that called it. Return the result as shown in [Step 3 of the example workflow](https://platform.claude.com/docs/en/agents-and-tools/tool-use/programmatic-tool-calling#step-3-provide-tool-result) so the code can finish. ## How programmatic tool calling works When you configure a tool to be callable from code execution and Claude determines that tool is needed: 1. Claude writes Python code that invokes the tool as a function, potentially including multiple tool calls and pre/post-processing logic 2. Claude runs this code in a sandboxed container through code execution 3. When a tool function is called, code execution pauses and the API returns a `tool_use` block 4. You provide the tool result, and code execution continues (intermediate results are not loaded into Claude's context window) 5. Once all code execution completes, Claude receives the final output and continues working on the task This approach is particularly useful for: * **Large data processing:** Filter or aggregate tool results before they reach Claude's context * **Multistep workflows:** Save tokens and latency by calling tools serially or in a loop without sampling Claude in-between tool calls * **Conditional logic:** Make decisions based on intermediate tool results Tools that allow a code execution caller are exposed to Claude's code as async Python functions, so Claude can run them in parallel with `asyncio.gather`. Each function takes a single dict of arguments and returns a string: the text of the `tool_result` you send back. Claude's code awaits these functions with top-level `await` and parses results that it needs as structured data, for example `rows = json.loads(await query_database({"sql": ""}))`. ## Core concepts ### The `allowed_callers` field The `allowed_callers` field specifies which contexts can invoke a tool: ```json { "name": "query_database", "description": "Execute a SQL query against the database", "input_schema": { // ... }, "allowed_callers": ["code_execution_20260120"] } ``` **Possible values:** * `["direct"]` - Claude is guided to call this tool directly (default if omitted) * `["code_execution_20260120"]` - Claude is guided to call this tool only from within code execution * `["direct", "code_execution_20260120"]` - Claude may call this tool directly or from within code execution Both `"code_execution_20260120"` and `"code_execution_20260521"` are accepted in `allowed_callers` and are interchangeable: a request using either code-execution tool version satisfies tools that list either caller. Response blocks always tag the caller as `code_execution_20260120` regardless of which version the request declared. Choose either `["direct"]` or `["code_execution_20260120"]` for each tool rather than enabling both, as this provides clearer guidance to Claude for how best to use the tool. `allowed_callers` controls how the tool is presented to Claude and is validated against `tool_choice`, but it is not a hard API-level block on direct invocation. Claude is strongly guided to respect it, but your client should still be prepared to handle a direct `tool_use` for any tool it defines. Do not rely on `allowed_callers` as a security boundary. ### The `caller` field in responses Every tool use block includes a `caller` field indicating how it was invoked: **Direct invocation (traditional tool use):** ```json { "type": "tool_use", "id": "toolu_abc123", "name": "query_database", "input": { "sql": "" }, "caller": { "type": "direct" } } ``` **Programmatic invocation:** ```json { "type": "tool_use", "id": "toolu_xyz789", "name": "query_database", "input": { "sql": "" }, "caller": { "type": "code_execution_20260120", "tool_id": "srvtoolu_abc123" } } ``` The `tool_id` is the `id` of the code execution `server_tool_use` block that made the call, so you can match each programmatic `tool_use` to the code execution run that produced it. ### Container lifecycle Programmatic tool calling uses the same containers as code execution: * **Container creation:** A new container is created for each request unless you reuse an existing one * **Container ID:** Returned in responses in the `container` field, along with an `expires_at` timestamp * **Reuse:** Pass the container ID back on the next request to keep state. While a programmatic tool call is waiting for your result, the container ID is required on that request, not optional: the API rejects the request without it. * **Expiration:** `expires_at` tells you how long the container has left. Idle containers are currently reclaimed after about 5 minutes, and no container can be reused more than 30 days after it was created. While Claude's code is waiting for a programmatic tool result, the pending call times out after about 4 minutes and raises a `TimeoutError` inside the code. Return each tool result well before the `expires_at` timestamp on the paused response. See [Container expiration during tool call](https://platform.claude.com/docs/en/agents-and-tools/tool-use/programmatic-tool-calling#container-expiration-during-tool-call). ## Example workflow Here's how a complete programmatic tool calling flow works: ### Step 1: Initial request Send a request with code execution and a tool that allows programmatic calling. To enable programmatic calling, add the `allowed_callers` field to your tool definition. Provide detailed descriptions of your tool's output format in the tool description. If you specify that the tool returns JSON, Claude attempts to deserialize and process the result in code. The more detail you provide about the output schema, the better Claude can handle the response programmatically. The request shape is identical to the [Quick start](https://platform.claude.com/docs/en/agents-and-tools/tool-use/programmatic-tool-calling#quick-start) example: include `code_execution` in your tools list, add `allowed_callers: ["code_execution_20260120"]` to any tool you want Claude to invoke from code, and send your user message. The remaining steps in this workflow use the user message `"Query customer purchase history from the last quarter and identify our top 5 customers by revenue"`. ### Step 2: API response with tool call Claude writes code that calls your tool. The API pauses and returns: ```json Output { "role": "assistant", "content": [ { "type": "text", "text": "I'll query the purchase history and analyze the results." }, { "type": "server_tool_use", "id": "srvtoolu_abc123", "name": "code_execution", "input": { "code": "import json\n\nrows = json.loads(await query_database({'sql': ''}))\ntop_customers = sorted(rows, key=lambda x: x['revenue'], reverse=True)[:5]\nprint(f'Top 5 customers: {top_customers}')" } }, { "type": "tool_use", "id": "toolu_def456", "name": "query_database", "input": { "sql": "" }, "caller": { "type": "code_execution_20260120", "tool_id": "srvtoolu_abc123" } } ], "container": { "id": "container_xyz789", "expires_at": "2026-01-20T14:30:00Z" }, "stop_reason": "tool_use" } ``` ### Step 3: Provide tool result Send the full conversation history plus your tool result. Three details matter on this request: * The user message that carries your result can contain only `tool_result` blocks. See [Message formatting restrictions](https://platform.claude.com/docs/en/agents-and-tools/tool-use/programmatic-tool-calling#message-formatting-restrictions). * Pass the `container` ID from the paused response. The API rejects a continuation that has pending programmatic tool calls but no container ID. * Send the same `tools` array as the original request. The code execution tool must still be present for the paused code to resume, and the tools you send on this request are the definitions Claude and the running code can use for the rest of the turn. ```bash cURL curl https://api.anthropic.com/v1/messages \ --header "x-api-key: $ANTHROPIC_API_KEY" \ --header "anthropic-version: 2023-06-01" \ --header "content-type: application/json" \ --data '{ "model": "claude-opus-5", "max_tokens": 4096, "container": "container_xyz789", "messages": [ { "role": "user", "content": "Query customer purchase history from the last quarter and identify our top 5 customers by revenue" }, { "role": "assistant", "content": [ { "type": "text", "text": "I'\''ll query the purchase history and analyze the results." }, { "type": "server_tool_use", "id": "srvtoolu_abc123", "name": "code_execution", "input": {"code": "..."} }, { "type": "tool_use", "id": "toolu_def456", "name": "query_database", "input": {"sql": ""}, "caller": { "type": "code_execution_20260120", "tool_id": "srvtoolu_abc123" } } ] }, { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_def456", "content": "[{\"customer_id\": \"C1\", \"revenue\": 45000}, {\"customer_id\": \"C2\", \"revenue\": 38000}]" } ] } ], "tools": [ { "type": "code_execution_20260120", "name": "code_execution" }, { "name": "query_database", "description": "Execute a SQL query against the sales database. Returns a list of rows as JSON objects.", "input_schema": { "type": "object", "properties": { "sql": { "type": "string", "description": "SQL query to execute" } }, "required": ["sql"] }, "allowed_callers": ["code_execution_20260120"] } ] }' ``` ```bash CLI ant messages create <<'YAML' model: claude-opus-5 max_tokens: 4096 container: container_xyz789 messages: - role: user content: >- Query customer purchase history from the last quarter and identify our top 5 customers by revenue - role: assistant content: - type: text text: I'll query the purchase history and analyze the results. - type: server_tool_use id: srvtoolu_abc123 name: code_execution input: code: "..." - type: tool_use id: toolu_def456 name: query_database input: sql: "" caller: type: code_execution_20260120 tool_id: srvtoolu_abc123 - role: user content: - type: tool_result tool_use_id: toolu_def456 content: >- [{"customer_id": "C1", "revenue": 45000}, {"customer_id": "C2", "revenue": 38000}, ...] # Same tools array as the original request tools: - type: code_execution_20260120 name: code_execution - name: query_database description: >- Execute a SQL query against the sales database. Returns a list of rows as JSON objects. input_schema: type: object properties: sql: type: string description: SQL query to execute required: - sql allowed_callers: - code_execution_20260120 YAML ``` ```python Python response = client.messages.create( model="claude-opus-5", max_tokens=4096, container="container_xyz789", # Reuse the container messages=[ { "role": "user", "content": "Query customer purchase history from the last quarter and identify our top 5 customers by revenue", }, { "role": "assistant", "content": [ { "type": "text", "text": "I'll query the purchase history and analyze the results.", }, { "type": "server_tool_use", "id": "srvtoolu_abc123", "name": "code_execution", "input": {"code": "..."}, }, { "type": "tool_use", "id": "toolu_def456", "name": "query_database", "input": {"sql": ""}, "caller": { "type": "code_execution_20260120", "tool_id": "srvtoolu_abc123", }, }, ], }, { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_def456", "content": '[{"customer_id": "C1", "revenue": 45000}, {"customer_id": "C2", "revenue": 38000}, ...]', } ], }, ], # Same tools array as the original request tools=[ {"type": "code_execution_20260120", "name": "code_execution"}, { "name": "query_database", "description": "Execute a SQL query against the sales database. Returns a list of rows as JSON objects.", "input_schema": { "type": "object", "properties": { "sql": {"type": "string", "description": "SQL query to execute"} }, "required": ["sql"], }, "allowed_callers": ["code_execution_20260120"], }, ], ) print(response) ``` ```typescript TypeScript const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 4096, container: "container_xyz789", // Reuse the container messages: [ { role: "user", content: "Query customer purchase history from the last quarter and identify our top 5 customers by revenue" }, { role: "assistant", content: [ { type: "text", text: "I'll query the purchase history and analyze the results." }, { type: "server_tool_use", id: "srvtoolu_abc123", name: "code_execution", input: { code: "..." } }, { type: "tool_use", id: "toolu_def456", name: "query_database", input: { sql: "" }, caller: { type: "code_execution_20260120", tool_id: "srvtoolu_abc123" } } ] }, { role: "user", content: [ { type: "tool_result", tool_use_id: "toolu_def456", content: '[{"customer_id": "C1", "revenue": 45000}, {"customer_id": "C2", "revenue": 38000}, ...]' } ] } ], // Same tools array as the original request tools: [ { type: "code_execution_20260120", name: "code_execution" }, { name: "query_database", description: "Execute a SQL query against the sales database. Returns a list of rows as JSON objects.", input_schema: { type: "object" as const, properties: { sql: { type: "string", description: "SQL query to execute" } }, required: ["sql"] }, allowed_callers: ["code_execution_20260120"] } ] }); console.log(response); ``` ```csharp C# AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 4096, Container = "container_xyz789", Messages = [ new() { Role = Role.User, Content = "Query customer purchase history from the last quarter and identify our top 5 customers by revenue" }, new() { Role = Role.Assistant, Content = new ContentBlock[] { new TextBlock { Text = "I'll query the purchase history and analyze the results." }, new ServerToolUseBlock { Id = "srvtoolu_abc123", Name = "code_execution", Input = new { code = "..." } }, new ToolUseBlock { Id = "toolu_def456", Name = "query_database", Input = new { sql = "" }, Caller = new ToolCaller { Type = "code_execution_20260120", ToolId = "srvtoolu_abc123" } } } }, new() { Role = Role.User, Content = new ContentBlockParam[] { new ToolResultBlockParam { ToolUseID = "toolu_def456", Content = "[{\"customer_id\": \"C1\", \"revenue\": 45000}, {\"customer_id\": \"C2\", \"revenue\": 38000}, ...]" } } } ], // Same tools array as the original request Tools = [ new CodeExecutionTool20260120(), new ToolUnion(new Tool() { Name = "query_database", Description = "Execute a SQL query against the sales database. Returns a list of rows as JSON objects.", InputSchema = new InputSchema() { Properties = new Dictionary { ["sql"] = JsonSerializer.SerializeToElement(new { type = "string", description = "SQL query to execute" }), }, Required = ["sql"], }, AllowedCallers = ["code_execution_20260120"] }), ] }; var message = await client.Messages.Create(parameters); Console.WriteLine(message); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 4096, Container: anthropic.MessageNewParamsContainerUnion{ OfString: anthropic.String("container_xyz789"), }, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Query customer purchase history from the last quarter and identify our top 5 customers by revenue")), { Role: anthropic.MessageParamRoleAssistant, Content: []anthropic.ContentBlockParamUnion{ anthropic.NewTextBlock("I'll query the purchase history and analyze the results."), {OfServerToolUse: &anthropic.ServerToolUseBlockParam{ ID: "srvtoolu_abc123", Name: anthropic.ServerToolUseBlockParamNameCodeExecution, Input: map[string]any{"code": "..."}, }}, {OfToolUse: &anthropic.ToolUseBlockParam{ ID: "toolu_def456", Name: "query_database", Input: map[string]any{"sql": ""}, Caller: anthropic.ServerToolUseBlockParamCallerUnion{ OfCodeExecution20260120: &anthropic.ServerToolCaller20260120Param{ ToolID: "srvtoolu_abc123", }, }, }}, }, }, { Role: anthropic.MessageParamRoleUser, Content: []anthropic.ContentBlockParamUnion{ {OfToolResult: &anthropic.ToolResultBlockParam{ ToolUseID: "toolu_def456", Content: []anthropic.ToolResultBlockParamContentUnion{ {OfText: &anthropic.TextBlockParam{ Text: `[{"customer_id": "C1", "revenue": 45000}, {"customer_id": "C2", "revenue": 38000}, ...]`, }}, }, }}, }, }, }, // Same tools array as the original request Tools: []anthropic.ToolUnionParam{ {OfCodeExecutionTool20260120: &anthropic.CodeExecutionTool20260120Param{}}, {OfTool: &anthropic.ToolParam{ Name: "query_database", Description: anthropic.String("Execute a SQL query against the sales database. Returns a list of rows as JSON objects."), InputSchema: anthropic.ToolInputSchemaParam{ Properties: map[string]any{ "sql": map[string]any{ "type": "string", "description": "SQL query to execute", }, }, Required: []string{"sql"}, }, AllowedCallers: []string{"code_execution_20260120"}, }}, }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java import com.anthropic.models.messages.CodeExecutionTool20260120; // ... void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(4096L) .container("container_xyz789") .addUserMessage("Query customer purchase history from the last quarter and identify our top 5 customers by revenue") .addAssistantMessageOfBlockParams(List.of( ContentBlockParam.ofText( TextBlockParam.builder() .text("I'll query the purchase history and analyze the results.") .build()), ContentBlockParam.ofServerToolUse( ServerToolUseBlockParam.builder() .id("srvtoolu_abc123") .name("code_execution") .input(JsonValue.from(Map.of("code", "..."))) .build()), ContentBlockParam.ofToolUse( ToolUseBlockParam.builder() .id("toolu_def456") .name("query_database") .input(JsonValue.from(Map.of("sql", ""))) .codeExecution20260120Caller("srvtoolu_abc123") .build()) )) .addUserMessageOfBlockParams(List.of( ContentBlockParam.ofToolResult( ToolResultBlockParam.builder() .toolUseId("toolu_def456") .content("[{\"customer_id\": \"C1\", \"revenue\": 45000}, {\"customer_id\": \"C2\", \"revenue\": 38000}, ...]") .build()) )) // Same tools array as the original request .addTool(CodeExecutionTool20260120.builder().build()) .addTool(Tool.builder() .name("query_database") .description("Execute a SQL query against the sales database. Returns a list of rows as JSON objects.") .inputSchema(InputSchema.builder() .properties(JsonValue.from(Map.of( "sql", Map.of( "type", "string", "description", "SQL query to execute" ) ))) .putAdditionalProperty("required", JsonValue.from(List.of("sql"))) .build()) .allowedCallers(List.of(Tool.AllowedCaller.of("code_execution_20260120"))) .build()) .build(); Message response = client.messages().create(params); IO.println(response); } ``` ```php PHP $client = new Client(); $message = $client->messages->create( maxTokens: 4096, messages: [ [ 'role' => 'user', 'content' => 'Query customer purchase history from the last quarter and identify our top 5 customers by revenue', ], [ 'role' => 'assistant', 'content' => [ [ 'type' => 'text', 'text' => "I'll query the purchase history and analyze the results.", ], [ 'type' => 'server_tool_use', 'id' => 'srvtoolu_abc123', 'name' => 'code_execution', 'input' => ['code' => '...'], ], [ 'type' => 'tool_use', 'id' => 'toolu_def456', 'name' => 'query_database', 'input' => ['sql' => ''], 'caller' => [ 'type' => 'code_execution_20260120', 'tool_id' => 'srvtoolu_abc123', ], ], ], ], [ 'role' => 'user', 'content' => [ [ 'type' => 'tool_result', 'tool_use_id' => 'toolu_def456', 'content' => '[{"customer_id": "C1", "revenue": 45000}, {"customer_id": "C2", "revenue": 38000}, ...]', ], ], ], ], model: 'claude-opus-5', container: 'container_xyz789', // Same tools array as the original request tools: [ [ 'type' => 'code_execution_20260120', 'name' => 'code_execution', ], [ 'name' => 'query_database', 'description' => 'Execute a SQL query against the sales database. Returns a list of rows as JSON objects.', 'input_schema' => [ 'type' => 'object', 'properties' => [ 'sql' => [ 'type' => 'string', 'description' => 'SQL query to execute', ], ], 'required' => ['sql'], ], 'allowed_callers' => ['code_execution_20260120'], ], ], ); echo $message; ``` ```ruby Ruby require "anthropic" client = Anthropic::Client.new message = client.messages.create( model: "claude-opus-5", max_tokens: 4096, container: "container_xyz789", messages: [ { role: "user", content: "Query customer purchase history from the last quarter and identify our top 5 customers by revenue" }, { role: "assistant", content: [ { type: "text", text: "I'll query the purchase history and analyze the results." }, { type: "server_tool_use", id: "srvtoolu_abc123", name: "code_execution", input: { code: "..." } }, { type: "tool_use", id: "toolu_def456", name: "query_database", input: { sql: "" }, caller: { type: "code_execution_20260120", tool_id: "srvtoolu_abc123" } } ] }, { role: "user", content: [ { type: "tool_result", tool_use_id: "toolu_def456", content: '[{"customer_id": "C1", "revenue": 45000}, {"customer_id": "C2", "revenue": 38000}, ...]' } ] } ], # Same tools array as the original request tools: [ { type: "code_execution_20260120", name: "code_execution" }, { name: "query_database", description: "Execute a SQL query against the sales database. Returns a list of rows as JSON objects.", input_schema: { type: "object", properties: { sql: { type: "string", description: "SQL query to execute" } }, required: ["sql"] }, allowed_callers: ["code_execution_20260120"] } ] ) puts message ``` ### Step 4: Next tool call or completion The code picks up where it paused and processes your result. Each continuation response either pauses again with more programmatic `tool_use` blocks, or completes the code execution and lets Claude continue the turn (Step 5). Check `stop_reason` and each `tool_use` block's `caller` to tell the two apart: a response that pauses for you has `stop_reason: "tool_use"` and a `tool_use` block whose `caller` names a code execution version, and you repeat Step 3 with a `tool_result` for every pending programmatic call in one user message. ### Step 5: Final response Once the code execution completes, Claude provides the final response: ```json Output { "content": [ { "type": "code_execution_tool_result", "tool_use_id": "srvtoolu_abc123", "content": { "type": "code_execution_result", "stdout": "Top 5 customers: [{'customer_id': 'C1', 'revenue': 45000}, {'customer_id': 'C2', 'revenue': 38000}, {'customer_id': 'C5', 'revenue': 32000}, {'customer_id': 'C8', 'revenue': 28500}, {'customer_id': 'C3', 'revenue': 24000}]", "stderr": "", "return_code": 0, "content": [] } }, { "type": "text", "text": "I've analyzed the purchase history from last quarter. Your top 5 customers generated $167,500 in total revenue, with Customer C1 leading at $45,000." } ], "stop_reason": "end_turn" } ``` ## Advanced patterns ### Batch processing with loops Claude can write code that processes multiple items efficiently: ```python regions = ["West", "East", "Central", "North", "South"] results = {} for region in regions: rows = json.loads(await query_database({"sql": f""})) results[region] = sum(row["revenue"] for row in rows) # Process results programmatically top_region = max(results.items(), key=lambda x: x[1]) print(f"Top region: {top_region[0]} with ${top_region[1]:,} in revenue") ``` This pattern: * Reduces model round-trips from N (one per region) to 1 * Processes large result sets programmatically before returning to Claude * Saves tokens by only returning aggregated conclusions instead of raw data ### Early termination Claude can stop processing as soon as success criteria are met: ```python endpoints = ["us-east", "eu-west", "apac"] for endpoint in endpoints: status = await check_health({"endpoint": endpoint}) if status == "healthy": print(f"Found healthy endpoint: {endpoint}") break # Stop early, don't check remaining ``` ### Conditional tool selection ```python path = "/tmp/example.txt" file_info = json.loads(await get_file_info({"path": path})) if file_info["size"] < 10000: content = await read_full_file({"path": path}) else: content = await read_file_summary({"path": path}) print(content) ``` ### Data filtering ```python server_id = "srv-01" log_text = await fetch_logs({"server_id": server_id}) errors = [line for line in log_text.splitlines() if "ERROR" in line] print(f"Found {len(errors)} errors") for error in errors[-10:]: # Only return last 10 errors print(error) ``` ## Response format ### Programmatic tool call When code execution calls a tool: ```json { "type": "tool_use", "id": "toolu_abc123", "name": "query_database", "input": { "sql": "" }, "caller": { "type": "code_execution_20260120", "tool_id": "srvtoolu_xyz789" } } ``` ### Tool result handling Your tool result is passed back to the running code: ```json { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_abc123", "content": "[{\"customer_id\": \"C1\", \"revenue\": 45000, \"orders\": 23}, {\"customer_id\": \"C2\", \"revenue\": 38000, \"orders\": 18}, ...]" } ] } ``` ### Code execution completion When all tool calls are satisfied and code completes: ```json { "type": "code_execution_tool_result", "tool_use_id": "srvtoolu_xyz789", "content": { "type": "code_execution_result", "stdout": "Analysis complete. Top 5 customers identified from 847 total records.", "stderr": "", "return_code": 0, "content": [] } } ``` ## Error handling ### Common errors | Error | Where it appears | Description | Solution | | ------------------------------------------ | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- | | `invalid_tool_input` | `error_code` on the `code_execution_tool_result` error block in the response | Invalid parameters were passed to the code execution tool | See the [code execution tool errors](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool#errors) | | `invalid_request_error` (on `tool_choice`) | HTTP 400 error response | `tool_choice` names a tool whose `allowed_callers` does not include `"direct"` | Either add `"direct"` to that tool's `allowed_callers`, or remove the tool from `tool_choice` and let Claude invoke it from code | ### Container expiration during tool call If your tool result doesn't arrive within about 4 minutes, the pending call raises a `TimeoutError` inside Claude's running code. Claude sees the error in `stderr` and typically retries the call: ```json { "type": "code_execution_tool_result", "tool_use_id": "srvtoolu_abc123", "content": { "type": "code_execution_result", "stdout": "", "stderr": "TimeoutError: Calling tool ['query_database'] timed out (no response after 270s).", "return_code": 0, "content": [] } } ``` To prevent timeouts: * Monitor the `expires_at` field in responses * Implement timeouts for your tool execution * Consider breaking long operations into smaller chunks ### Tool execution errors If your tool returns an error: ```json { "type": "tool_result", "tool_use_id": "toolu_abc123", "content": "Error: Query timeout - table lock exceeded 30 seconds" } ``` Claude's code receives this error and can handle it appropriately. ## Constraints and limitations ### Feature incompatibilities * **Structured outputs:** Tools with `strict: true` are not supported with programmatic calling * **Tool choice:** You cannot force programmatic calling of a specific tool through `tool_choice` * **Parallel tool use:** `disable_parallel_tool_use: true` is not supported with programmatic calling ### Input schema limitations Custom tools whose `input_schema` contains a recursive `$ref` (a reference cycle, such as a schema that refers to itself) cannot be enabled for programmatic calling. Including a code execution tool version in `allowed_callers` for such a tool causes the request to fail with a `400 invalid_request_error` whose message contains `Circular $ref detected`. The same schema is accepted for direct tool calling. To work around this, do one of the following: * Keep the tool direct-only by omitting `allowed_callers` (or setting it to `["direct"]`). Other tools in the same request can still use programmatic calling. * Remove the cycle from the schema. For example, unroll the recursion to a fixed depth and describe any deeper nesting in the `description` of the innermost level, or replace the recursive property with a plain `{"type": "object"}` whose `description` explains the expected shape. ### Tool restrictions The following tools cannot be called programmatically: * Tools provided by an [MCP connector](https://platform.claude.com/docs/en/agents-and-tools/mcp-connector) ### Message formatting restrictions When responding to programmatic tool calls, there are strict formatting requirements: **Tool result only responses:** If there are pending programmatic tool calls waiting for results, your response message must contain **only** `tool_result` blocks. You cannot include any text content, even after the tool results. Invalid - Cannot include text when responding to programmatic tool calls: ```json { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_01", "content": "[{\"customer_id\": \"C1\", \"revenue\": 45000}]" }, { "type": "text", "text": "What should I do next?" } ] } ``` Valid - Only tool results when responding to programmatic tool calls: ```json { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_01", "content": "[{\"customer_id\": \"C1\", \"revenue\": 45000}]" } ] } ``` This restriction only applies when responding to programmatic (code execution) tool calls. For regular client-side tool calls, you can include text content after tool results. **Text-only tool result content:** The `content` of each `tool_result` that answers a programmatic call must be a string or `text` blocks. Image, document, and other content block types are rejected. ### Rate limits Programmatic tool calls are subject to the same rate limits as regular tool calls. Each tool call from code execution counts as a separate invocation. ### Validate tool results before use When implementing user-defined tools that will be called programmatically: * **Tool results are returned as strings:** They can contain any content, including code snippets or executable commands that may be processed by the execution environment. * **Validate external tool results:** If your tool returns data from external sources or accepts user input, be aware of code injection risks if the output will be interpreted or executed as code. ## Token efficiency Programmatic tool calling reduces token consumption in three ways: * **Tool results from programmatic calls are not added to Claude's context** - only the final code output is * **Intermediate processing happens in code** - filtering, aggregation, and other transformations don't consume model tokens * **Multiple tool calls in one code execution** - reduces overhead compared to separate model turns For example, calling 10 tools directly uses \~10x the tokens of calling them programmatically and returning a summary. In Anthropic's internal evaluations on a production Claude model: * On a 75-tool project-management agent benchmark, enabling programmatic tool calling reduced billed input tokens by roughly 38% with no change in task accuracy. * On [τ²-bench](https://arxiv.org/abs/2506.07982) (airline, retail, and telecom domains), where each turn makes one or two sequential tool calls, programmatic tool calling left scores unchanged and cost roughly 8% more. Sequential single-call workflows do not benefit. * Across production API traffic, requests whose `tools` array contains 10 to 49 tool definitions see typical token savings of 20% to 40% with programmatic tool calling enabled. Actual savings vary with workload shape. See [When to use programmatic calling](https://platform.claude.com/docs/en/agents-and-tools/tool-use/programmatic-tool-calling#when-to-use-programmatic-calling). ## Usage and pricing Programmatic tool calling uses the same pricing as code execution. See the [code execution pricing](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool#usage-and-pricing) for details. Token counting for programmatic tool calls: Tool results from programmatic invocations do not count toward your input/output token usage. Only the final code execution result and Claude's response count. ## Best practices ### Tool design * **Provide detailed output descriptions:** Because Claude deserializes tool results in code, document the format (JSON structure and field types) * **Return structured data:** JSON or other machine-readable formats work best for programmatic processing * **Keep responses concise:** Return only necessary data to minimize processing overhead ### When to use programmatic calling Programmatic tool calling trades a small fixed overhead (container startup, script generation) for large savings on tool-result tokens and model round-trips. Whether that trade pays off depends on workload shape. **Strong fit:** * Fan-out or parallel operations across many items (for example, checking 50 endpoints or looking up 20 records) * Large tool results that can be filtered, aggregated, or summarized before reaching Claude's context * Agentic search and retrieval, where iterative querying and result filtering dominate the workflow **Weak fit:** * Strictly sequential workflows where each call depends on Claude reasoning over the previous result, because the script cannot skip the model round-trip in that case * A small number of tool calls with small responses, especially on the first turn of a conversation, where container and script overhead can exceed the savings * Tools that require immediate user feedback between calls If you are unsure, measure billed input tokens with and without `allowed_callers` on a representative sample of your traffic before enabling it broadly. ### Performance optimization * **Reuse containers** when making multiple related requests to maintain state * **Batch similar operations** in a single code execution when possible ## Troubleshooting ### Common issues **`invalid_request_error` when setting `tool_choice`** * `tool_choice` cannot name a tool whose `allowed_callers` omits `"direct"`. Either add `"direct"` to that tool's `allowed_callers`, or remove the tool from `tool_choice` and let Claude invoke it from code. **Container expiration** * Respond to each programmatic tool call well before the paused response's `expires_at` timestamp. Claude's code stops waiting for a result after about 4 minutes, and idle containers are currently reclaimed after about 5 minutes. * Consider implementing faster tool execution **Tool result not parsed correctly** * Ensure your tool returns string data that Claude can deserialize * Provide clear output format documentation in your tool description ### Debugging tips 1. **Log all tool calls and results** to track the flow 2. **Check the `caller` field** to confirm programmatic invocation 3. **Monitor container IDs** to ensure proper reuse 4. **Test tools independently** before enabling programmatic calling ## Why programmatic tool calling works Claude is trained on large amounts of code, so presenting tools as callable Python functions lets it use that strength: * **Tool composition:** Chained calls, loops, and conditionals are ordinary Python control flow instead of a series of model round trips * **Result processing:** Claude's code filters and aggregates large tool outputs, or writes them to files, and only the final output enters the context window * **Latency:** The model is not re-sampled between the tool calls inside one code execution ## Alternative implementations Programmatic tool calling is a generalizable pattern that can also be implemented on your own infrastructure. Here's how the approaches compare: ### Client-side direct execution Provide Claude with a code execution tool and describe what functions are available in that environment. When Claude invokes the tool with code, your application executes it locally where those functions are defined. **Advantages:** * Minimal re-architecting of your application * Full control over the environment and instructions **Disadvantages:** * Executes untrusted code outside of a sandbox * Tool invocations can be vectors for code injection **Use when:** Your application can safely execute arbitrary code, you want the smallest implementation, and Anthropic's managed offering doesn't fit your needs. ### Self-managed sandboxed execution Same approach from Claude's perspective, but code runs in a sandboxed container with security restrictions (for example, no network egress). If your tools require external resources, you'll need a protocol for executing tool calls outside the sandbox. **Advantages:** * Safe programmatic tool calling on your own infrastructure * Full control over the execution environment **Disadvantages:** * Complex to build and maintain * Requires managing both infrastructure and inter-process communication **Use when:** Security is critical and Anthropic's managed solution doesn't fit your requirements. ### Anthropic-managed execution Anthropic's programmatic tool calling is a managed version of sandboxed execution with an opinionated Python environment tuned for Claude. Anthropic handles container management, code execution, and secure tool invocation communication. **Advantages:** * Safe and secure by default * Enabled with a tool definition, with no infrastructure to run * Environment and instructions optimized for Claude Consider using Anthropic's managed solution if you're using the Claude API, [Claude Platform on AWS](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws), or [Microsoft Foundry](https://platform.claude.com/docs/en/build-with-claude/claude-in-microsoft-foundry). On Microsoft Foundry, programmatic tool calling requires a [Hosted on Anthropic deployment](https://platform.claude.com/docs/en/build-with-claude/claude-in-microsoft-foundry#additional-features-not-supported-when-hosted-on-azure). ## Data retention Programmatic tool calling is built on the code execution infrastructure and uses the same sandbox containers. Container data, including execution artifacts and outputs, is retained for up to 30 days. For ZDR eligibility across all features, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention). ## Next steps Stream tool inputs without server-side JSON buffering for latency-sensitive applications. Run Python and bash code in a sandboxed container to analyze data, generate files, and iterate on solutions. Connect Claude to external tools and APIs. See where tools execute, when Claude calls them, and which tool fits your task. Specify tool schemas, write effective descriptions, and control when Claude calls your tools. --- title: Tool combinations url: https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-combinations description: Common Anthropic tool pairings for research agents, coding agents, and long-running agents. --- Anthropic-provided tools are designed to work together. Common agent patterns pair tools that cover complementary stages of a workflow: one tool gathers or discovers, another processes or acts. The combinations below are starting points, not prescriptions. Mix them to fit your task. Each snippet shows only the `tools` array. See [Handle tool calls](https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls) for the full request shape. ## Research agent: web\_search + code\_execution Search finds sources; code execution analyzes and synthesizes. Claude searches for data, then writes Python to process, tabulate, or visualize it. This pairing is a good fit for questions that require both up-to-date information and nontrivial computation over that information, such as "compare this quarter's earnings across the top five cloud providers." ```json { "tools": [ { "type": "web_search_20260209", "name": "web_search" }, { "type": "code_execution_20260521", "name": "code_execution" } ] } ``` The flow is typically search, then execute, then optionally search again if the first pass surfaced a gap. Code execution runs server-side, so there's no client-side sandbox to manage. ## Coding agent: text\_editor + bash The text editor reads and modifies files; bash runs tests and build commands. This is the canonical software-development loop: inspect the code, make an edit, run the tests, repeat. Both tools are client-executed, so your application controls which files and commands are accessible. ```json { "tools": [ { "type": "text_editor_20250728", "name": "str_replace_based_edit_tool" }, { "type": "bash_20250124", "name": "bash" } ] } ``` Pair this with a constrained working directory and a command allowlist if the agent operates on untrusted code. See [Text editor tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/text-editor-tool) and [Bash tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/bash-tool) for the execution contracts. ## Cite-then-fetch: web\_search + web\_fetch Search surfaces candidate URLs; fetch retrieves full page content for the relevant ones. This avoids fetching everything upfront. Claude runs a search, inspects the snippets, picks the two or three results that actually look relevant, and fetches only those. ```json { "tools": [ { "type": "web_search_20260209", "name": "web_search" }, { "type": "web_fetch_20260209", "name": "web_fetch" } ] } ``` This pairing is useful when the answer lives in long-form content (documentation pages, articles, specifications) that a search snippet can't fully capture. Fetch pulls the complete page so Claude can cite specific passages. ## Long-running agent: memory + any toolset Memory persists state across conversations; the other tools do the work. Add memory to any agent that needs to remember prior sessions, such as a support agent that recalls a customer's earlier issues or a project assistant that tracks decisions made last week. ```json { "tools": [{ "type": "memory_20250818", "name": "memory" }] } ``` Add your other tools alongside `memory` in the same array. Memory is orthogonal to the rest of your toolset. It doesn't change how other tools behave; it gives Claude a place to write down and later retrieve facts that would otherwise be lost when the context window resets. See [Memory tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/memory-tool) for the storage model. ## All-in-one: computer\_use The computer use tool subsumes most others by operating a full desktop. Claude sees screenshots and issues mouse and keyboard actions, which means it can drive any application a human can. Use this when the task requires arbitrary GUI interaction that more specific tools can't reach: legacy software without an API, visual verification steps, or workflows that span multiple desktop apps. ```json { "tools": [ { "type": "computer_20250124", "name": "computer", "display_width_px": 1280, "display_height_px": 800 } ] } ``` Computer use is the most general option and also the slowest, because every action requires a screenshot roundtrip. Prefer narrower tools when they cover your use case, and reach for computer use when nothing else fits. See [Computer use tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/computer-use-tool) for the sandbox setup. ## Next steps Full catalog of Anthropic-provided tools with type strings and parameters. How tool use works and when to use Anthropic tools versus defining your own. --- title: Tool reference url: https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-reference description: Directory of Anthropic-provided tools and reference for optional tool definition properties. --- This page is a reference for the tools Anthropic provides and the optional properties you can set on any tool definition. For a conceptual introduction to tool use, see [Tool use with Claude](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview). For guidance on implementing tool use in your application, see [Define tools](https://platform.claude.com/docs/en/agents-and-tools/tool-use/define-tools). ## Anthropic-provided tools Anthropic provides two kinds of tools: **server tools** that execute on Anthropic's infrastructure, and **client tools** where Anthropic defines the schema but your application handles execution. Both kinds appear in your request's `tools` array alongside any user-defined tools. | Tool | `type` | Execution | Status | | -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | --------- | --------------------------------------------------------- | | [Web search tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-search-tool) | `web_search_20260318` `web_search_20260209` `web_search_20250305` | Server | GA | | [Web fetch tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-fetch-tool) | `web_fetch_20260318` `web_fetch_20260309` `web_fetch_20260209` `web_fetch_20250910` | Server | GA | | [Code execution tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool) | `code_execution_20260521` `code_execution_20260120` `code_execution_20250825` | Server | GA | | [Advisor tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/advisor-tool) | `advisor_20260301` | Server | Beta: `advisor-tool-2026-03-01` | | [Tool search tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool) | `tool_search_tool_regex_20251119` `tool_search_tool_bm25_20251119` | Server | GA | | [MCP connector](https://platform.claude.com/docs/en/agents-and-tools/mcp-connector) | `mcp_toolset` | Server | Beta: `mcp-client-2025-11-20` | | [Memory tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/memory-tool) | `memory_20250818` | Client | GA | | [Bash tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/bash-tool) | `bash_20250124` | Client | GA | | [Text editor tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/text-editor-tool) | `text_editor_20250728` `text_editor_20250124` | Client | GA | | [Computer use tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/computer-use-tool) | `computer_20251124` `computer_20250124` | Client | Beta: `computer-use-2025-11-24` `computer-use-2025-01-24` | For model compatibility, see each tool's page. Supported models vary by tool and by tool version. The tool search `type` values also accept undated aliases: `tool_search_tool_regex` and `tool_search_tool_bm25`. These resolve to the latest dated version. ### Tool versioning Most Anthropic-provided tools carry a `_YYYYMMDD` suffix in the `type` string. A new version is released when the tool's behavior, schema, or model support changes. Older versions remain available so that existing integrations continue to work. When a tool has multiple active versions, the relationship between them varies: * **Capability-keyed:** `web_search_20260209` and `web_fetch_20260209` add dynamic content filtering over their predecessors; `web_fetch_20260309` adds a cache-bypass option; `web_search_20260318` and `web_fetch_20260318` add response-inclusion control. `code_execution_20260120` adds [programmatic tool calling](https://platform.claude.com/docs/en/agents-and-tools/tool-use/programmatic-tool-calling) from within the sandbox; `code_execution_20260521` discloses the per-cell time limit in the tool description. In each case, both the new and old versions are current; which one you use depends on whether you need the new capability. * **Model-keyed:** `text_editor_20250728` is for Claude 4 and later models and `text_editor_20250124` is for earlier models. The version you use depends on the model you target. * **Variant, not version:** `tool_search_tool_regex_20251119` and `tool_search_tool_bm25_20251119` are two search algorithms released together. Neither supersedes the other. * **Legacy:** `code_execution_20250522` supports only Python. `code_execution_20250825` adds Bash and file operations. The `mcp_toolset` type is not date-versioned; versioning is carried in the `anthropic-beta` header instead. ## Tool definition properties Every tool in the `tools` array, including user-defined tools, accepts optional properties that control how the tool is loaded, who can call it, and how its inputs are validated. These properties compose: you can set `defer_loading` and `cache_control` and `strict` on the same tool. | Property | Purpose | Available on | Detailed guide | | ----------------------- | --------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `cache_control` | Set a prompt-cache breakpoint at this tool definition | All tools | [Prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) | | `strict` | Guarantee schema validation on tool names and inputs | All tools except `mcp_toolset` | [Strict tool use](https://platform.claude.com/docs/en/agents-and-tools/tool-use/strict-tool-use) | | `defer_loading` | Exclude the tool from the initial system prompt; load it on demand when tool search returns a `tool_reference` for it | All tools (for `mcp_toolset`, see [tool configuration](https://platform.claude.com/docs/en/agents-and-tools/mcp-connector#mcp-toolset-configuration)) | [Tool search tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool) | | `allowed_callers` | Restrict which callers can call the tool | All tools except `mcp_toolset` | [Programmatic tool calling](https://platform.claude.com/docs/en/agents-and-tools/tool-use/programmatic-tool-calling#the-allowed-callers-field) | | `input_examples` | Provide example input objects to help Claude understand how to call the tool | User-defined and Anthropic-schema client tools. Not available on server tools. | [Define tools](https://platform.claude.com/docs/en/agents-and-tools/tool-use/define-tools#providing-tool-use-examples) | | `eager_input_streaming` | Enable fine-grained input streaming (`true`) or keep standard buffered streaming (`false`) for this tool | User-defined tools only | [Fine-grained tool streaming](https://platform.claude.com/docs/en/agents-and-tools/tool-use/fine-grained-tool-streaming) | ### `allowed_callers` values `allowed_callers` is an array that accepts any combination of: | Value | Meaning | | --------------------------- | ----------------------------------------------------------------------------------------------------------------- | | `"direct"` | The model can call this tool directly in a `tool_use` block. This is the default if `allowed_callers` is omitted. | | `"code_execution_20260120"` | Code running inside a `code_execution_20260120` or later sandbox can call this tool. | Both `"code_execution_20260120"` and `"code_execution_20260521"` are accepted in `allowed_callers` and are interchangeable: a request using either code-execution tool version satisfies tools that list either caller. Response blocks always tag the caller as `code_execution_20260120` regardless of which version the request declared. Omitting `"direct"` from the array (for example, `"allowed_callers": ["code_execution_20260120"]`) guides Claude to call the tool only from within code execution. The response's `tool_use` block includes a `caller` field that identifies which caller called the tool. See [Programmatic tool calling](https://platform.claude.com/docs/en/agents-and-tools/tool-use/programmatic-tool-calling#the-allowed-callers-field) for the full treatment, including the `caller` response shape and error behavior. ### `defer_loading` and prompt caching Tools with `defer_loading: true` are stripped from the rendered tools section before the cache key is computed. They don't appear in the system-prompt prefix at all. When tool search discovers a deferred tool and returns a `tool_reference` for it, the tool's full definition is expanded inline at that point in the conversation body, not in the prefix. This means `defer_loading: true` preserves your prompt cache. You can add deferred tools to a request without invalidating an existing cache entry, and the cache remains valid across the turn where the tool is discovered and the turn where it's called. For how to combine `defer_loading` with `cache_control` breakpoints, see the [Tool search tool prompt caching guidance](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool#prompt-caching). --- title: Tool use with prompt caching url: https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-use-with-prompt-caching description: Cache tool definitions across turns and understand what invalidates your cache. --- This page covers prompt caching for tool definitions: where to place `cache_control` breakpoints, how `defer_loading` preserves your cache, and what invalidates it. For general prompt caching, see [Prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching). ## cache\_control on tool definitions Place `cache_control: {"type": "ephemeral"}` on the last tool in your `tools` array. This caches the entire tool-definitions prefix, from the first tool through the marked breakpoint: ```json { "tools": [ { "name": "get_weather", "description": "Get the current weather in a given location", "input_schema": { "type": "object", "properties": { "location": { "type": "string" } }, "required": ["location"] } }, { "name": "get_time", "description": "Get the current time in a given time zone", "input_schema": { "type": "object", "properties": { "timezone": { "type": "string" } }, "required": ["timezone"] }, "cache_control": { "type": "ephemeral" } } ] } ``` For `mcp_toolset`, the `cache_control` breakpoint lands on the last tool in the set. You don't control tool order within an MCP toolset, so place the breakpoint on the `mcp_toolset` entry itself and the API applies it to the final expanded tool. ## defer\_loading and cache preservation Deferred tools are not included in the system-prompt prefix. When the model discovers a deferred tool through [tool search](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool), the definition is appended inline as a `tool_reference` block in the conversation history. The prefix is untouched, so prompt caching is preserved. This means adding tools dynamically through tool search does not break your cache. You can start a conversation with a small set of always-loaded tools (cached), let the model discover additional tools as needed, and keep the same cache hit across every turn. `defer_loading` also acts independently of grammar construction for [strict mode](https://platform.claude.com/docs/en/agents-and-tools/tool-use/strict-tool-use). The grammar builds from the full toolset regardless of which tools are deferred, so prompt caching and grammar caching are both preserved when tools load dynamically. ## What invalidates your cache The cache follows a prefix hierarchy (`tools` → `system` → `messages`), so a change at one level invalidates that level and everything after it: | Change | Invalidates | | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Modifying tool definitions | Entire cache (tools, system, messages) | | Toggling web search or citations | System and messages caches | | Changing `tool_choice` | Messages cache | | Changing `disable_parallel_tool_use` | Messages cache | | Toggling images present/absent | Messages cache | | Changing thinking parameters | Messages cache always; tool and system caches too on models that render the thinking configuration ahead of them ([details](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-and-prompt-caching)) | | Changing `output_config.effort` | Same as thinking parameters; setting the model's default explicitly is equivalent to omitting it | If you need to vary `tool_choice` mid-conversation, consider placing cache breakpoints before the variation point. ## Server tool results are cached automatically When your request has prompt caching enabled and Claude uses a [server tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/server-tools) such as web search, web fetch, or code execution, the API automatically places a cache breakpoint on the server tool result before running the next iteration of the agentic loop. This lets later iterations within the same request read the growing prefix from cache instead of reprocessing it. This automatic breakpoint always uses the default 5-minute TTL, independent of any TTL you set on your own `cache_control` markers. In the response `usage`, these writes appear under `cache_creation.ephemeral_5m_input_tokens`, so you may see 5-minute cache writes even when every `cache_control` you set uses a 1-hour TTL. This behavior only applies when your request already has at least one `cache_control` marker. Requests without prompt caching do not receive the automatic breakpoint. ## Per-tool interaction table | Tool | Caching considerations | | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | | [Web search](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-search-tool) | Enabling or disabling invalidates the system and messages caches | | [Web fetch](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-fetch-tool) | Enabling or disabling invalidates the system and messages caches | | [Code execution](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool) | Container state is independent of prompt cache | | [Tool search](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool) | Discovered tools load as `tool_reference` blocks, preserving prefix cache | | [Computer use](https://platform.claude.com/docs/en/agents-and-tools/tool-use/computer-use-tool) | Screenshot presence affects messages cache | | [Text editor](https://platform.claude.com/docs/en/agents-and-tools/tool-use/text-editor-tool) | Standard client tool, no special caching interaction | | [Bash](https://platform.claude.com/docs/en/agents-and-tools/tool-use/bash-tool) | Standard client tool, no special caching interaction | | [Memory](https://platform.claude.com/docs/en/agents-and-tools/tool-use/memory-tool) | Standard client tool, no special caching interaction | ## Next steps Learn the full prompt caching model, including TTLs and pricing. Load tools on demand without breaking your cache. Browse all available tools and their parameters. ### Context management --- title: Build an orchestration mode url: https://platform.claude.com/docs/en/build-with-claude/mid-conversation-effort-example description: Build a session-level mode that grants standing consent for multiagent fan-out, switched on and off with mid-conversation system messages. --- An orchestration mode is a session-level switch: when it is on, the model puts maximum thoroughness behind every substantive request, scouting the task itself and then fanning work out to parallel subagents by default. When it is off, the same orchestration tool goes back to per-request opt-in. The mode is not an API parameter. It is built entirely from documented pieces: 1. **An effort level:** requests run at a documented [Effort](https://platform.claude.com/docs/en/build-with-claude/effort) value such as `xhigh`. There is no hidden level above the ones on that page. This example sets effort at the top level of each request, which needs no beta header. 2. **A mode reminder:** a [mid-conversation system message](https://platform.claude.com/docs/en/build-with-claude/mid-conversation-system-messages) tells the model the mode is active, with a one-line refresher every several turns and an exit notice when the mode is turned off. The top-level `system` field never changes, so the cached prefix stays intact. 3. **Standing consent in the tool description:** the orchestration tool's description states that while the mode is on, the model should author and run a workflow for every substantive task without asking first. This example uses mid-conversation system messages; for the models and platforms that support them, see [Mid-conversation system messages](https://platform.claude.com/docs/en/build-with-claude/mid-conversation-system-messages). The fan-out itself multiplies token usage: a single request can spawn many subagent conversations, so reserve the mode for work that justifies the cost. ## Set up the loop The example is a single file. The constants control the effort level, the fan-out shape, and how often the mode refresher is re-sent. `MAX_CONCURRENT` caps how many subagents run at the same time (the PHP port is sequential and ignores it); `MAX_TOTAL_SUBTASKS` caps how many the model may queue in a single Workflow call. Splitting the two lets the model plan a large backlog without launching it all at once. The `DOC_TEST_MODE` check caps the loops to a single turn when that environment variable is set, so the automated docs harness can validate that the file compiles and finishes quickly without running the full orchestration; leave it unset when running the example yourself. ```python Python import atexit import concurrent.futures import hashlib import json import os import shutil import subprocess import sys import tempfile import threading import anthropic client = anthropic.Anthropic() MODEL = "claude-opus-5" EFFORT = "xhigh" SYSTEM_PROMPT = "You are a helpful general-purpose agent. Answer the user's request directly." REQUEST_TIMEOUT_SECONDS = 600 BASH_TIMEOUT_SECONDS = 60 TOOL_RESULT_MAX_CHARS = 8000 MAX_CONCURRENT = 10 DOC_TEST_MODE = bool(os.environ.get("DOC_TEST_MODE")) MAX_TOTAL_SUBTASKS = 2 if DOC_TEST_MODE else 200 MAX_SUBAGENT_TURNS = 1 if DOC_TEST_MODE else 15 MAX_MAIN_TURNS = 1 if DOC_TEST_MODE else 30 TURNS_BETWEEN_REFRESHERS = 10 JOURNAL_PATH = os.environ.get("ORCH_JOURNAL") or "orchestration_journal.json" ``` ```typescript TypeScript import { exec } from "node:child_process"; import { createHash } from "node:crypto"; import { rmSync } from "node:fs"; import { mkdtemp, readFile, rename, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { promisify } from "node:util"; import Anthropic from "@anthropic-ai/sdk"; const client = new Anthropic(); const MODEL = "claude-opus-5"; const EFFORT = "xhigh"; const SYSTEM_PROMPT = "You are a helpful general-purpose agent. Answer the user's request directly."; const REQUEST_TIMEOUT_SECONDS = 600; const BASH_TIMEOUT_SECONDS = 60; const TOOL_RESULT_MAX_CHARS = 8000; const MAX_CONCURRENT = 10; const DOC_TEST_MODE = Boolean(process.env.DOC_TEST_MODE); const MAX_TOTAL_SUBTASKS = DOC_TEST_MODE ? 2 : 200; const MAX_SUBAGENT_TURNS = DOC_TEST_MODE ? 1 : 15; const MAX_MAIN_TURNS = DOC_TEST_MODE ? 1 : 30; const TURNS_BETWEEN_REFRESHERS = 10; const JOURNAL_PATH = process.env.ORCH_JOURNAL || "orchestration_journal.json"; ``` ```csharp C# using System.Diagnostics; using System.Security.Cryptography; using System.Text; using System.Text.Json; using Anthropic; using Anthropic.Models.Messages; AnthropicClient client = new(); const Model model = Model.ClaudeOpus5; var effort = Effort.Xhigh; const string systemPrompt = "You are a helpful general-purpose agent. Answer the user's request directly."; const int requestTimeoutSeconds = 600; // The other ports stream with max_tokens 64000. This port uses non-streaming // Messages.Create, and the API rejects non-streaming requests at that size. // 8192 is the non-streaming ceiling for Opus 4.0 and 4.1 and a conservative // choice for newer Opus models. const int requestMaxTokens = 8192; const int bashTimeoutSeconds = 60; const int toolResultMaxChars = 8000; const int maxConcurrent = 10; var docTestMode = Environment.GetEnvironmentVariable("DOC_TEST_MODE") is { Length: > 0 }; int maxTotalSubtasks = docTestMode ? 2 : 200; int maxSubagentTurns = docTestMode ? 1 : 15; int maxMainTurns = docTestMode ? 1 : 30; const int turnsBetweenRefreshers = 10; var journalPath = Environment.GetEnvironmentVariable("ORCH_JOURNAL") is { Length: > 0 } p ? p : "orchestration_journal.json"; ``` ```go Go import ( "bytes" "cmp" "context" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "log" "os" "os/exec" "path/filepath" "strings" "sync" "time" "github.com/anthropics/anthropic-sdk-go" ) var client = anthropic.NewClient() const ( modelID = anthropic.ModelClaudeOpus5 effort = anthropic.OutputConfigEffortXhigh systemPrompt = "You are a helpful general-purpose agent. Answer the user's request directly." requestTimeoutSeconds = 600 bashTimeoutSeconds = 60 toolResultMaxChars = 8000 maxConcurrent = 10 turnsBetweenRefreshers = 10 ) var ( docTestMode = os.Getenv("DOC_TEST_MODE") != "" maxTotalSubtasks = ifTest(2, 200) maxSubagentTurns = ifTest(1, 15) maxMainTurns = ifTest(1, 30) journalPath = cmp.Or(os.Getenv("ORCH_JOURNAL"), "orchestration_journal.json") ) func ifTest(test, normal int) int { if docTestMode { return test } return normal } ``` ```java Java import com.anthropic.client.AnthropicClient; import com.anthropic.client.okhttp.AnthropicOkHttpClient; import com.anthropic.core.JsonValue; import com.anthropic.core.RequestOptions; import com.anthropic.helpers.MessageAccumulator; import com.anthropic.models.messages.ContentBlock; import com.anthropic.models.messages.ContentBlockParam; import com.anthropic.models.messages.Message; import com.anthropic.models.messages.MessageCreateParams; import com.anthropic.models.messages.MessageParam; import com.anthropic.models.messages.Model; import com.anthropic.models.messages.OutputConfig; import com.anthropic.models.messages.StopReason; import com.anthropic.models.messages.TextBlock; import com.anthropic.models.messages.Tool; import com.anthropic.models.messages.ToolBash20250124; import com.anthropic.models.messages.ToolResultBlockParam; import com.anthropic.models.messages.ToolUseBlock; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.security.MessageDigest; import java.time.Duration; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; import java.util.HexFormat; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.concurrent.Callable; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; import java.util.stream.IntStream; AnthropicClient client = AnthropicOkHttpClient.fromEnv(); static final Model MODEL = Model.CLAUDE_OPUS_5; static final boolean DOC_TEST_MODE = !Objects.requireNonNullElse(System.getenv("DOC_TEST_MODE"), "").isEmpty(); static final OutputConfig.Effort EFFORT = OutputConfig.Effort.XHIGH; static final String SYSTEM_PROMPT = "You are a helpful general-purpose agent. Answer the user's request directly."; static final int REQUEST_TIMEOUT_SECONDS = 600; static final RequestOptions REQUEST_OPTIONS = RequestOptions.builder().timeout(Duration.ofSeconds(REQUEST_TIMEOUT_SECONDS)).build(); static final int BASH_TIMEOUT_SECONDS = 60; static final int TOOL_RESULT_MAX_CHARS = 8000; static final int MAX_CONCURRENT = 10; static final int MAX_TOTAL_SUBTASKS = DOC_TEST_MODE ? 2 : 200; static final int MAX_SUBAGENT_TURNS = DOC_TEST_MODE ? 1 : 15; static final int MAX_MAIN_TURNS = DOC_TEST_MODE ? 1 : 30; static final int TURNS_BETWEEN_REFRESHERS = 10; static final Path JOURNAL_PATH = Path.of(Optional.ofNullable(System.getenv("ORCH_JOURNAL")) .filter(s -> !s.isEmpty()).orElse("orchestration_journal.json")); ``` ```php PHP use Anthropic\Client; use Anthropic\Messages\TextBlock; use Anthropic\Messages\ToolUseBlock; $client = new Client(); const MODEL = 'claude-opus-5'; define('DOC_TEST_MODE', (string) getenv('DOC_TEST_MODE') !== ''); const EFFORT = 'xhigh'; const SYSTEM_PROMPT = 'You are a helpful general-purpose agent. Answer the user\'s request directly.'; const REQUEST_TIMEOUT_SECONDS = 600; const BASH_TIMEOUT_SECONDS = 60; const TOOL_RESULT_MAX_CHARS = 8000; const MAX_CONCURRENT = 10; define('MAX_TOTAL_SUBTASKS', DOC_TEST_MODE ? 2 : 200); define('MAX_SUBAGENT_TURNS', DOC_TEST_MODE ? 1 : 15); define('MAX_MAIN_TURNS', DOC_TEST_MODE ? 1 : 30); const TURNS_BETWEEN_REFRESHERS = 10; define('JOURNAL_PATH', getenv('ORCH_JOURNAL') ?: 'orchestration_journal.json'); ``` ```ruby Ruby require "anthropic" require "digest" require "fileutils" require "json" require "open3" require "tmpdir" CLIENT = Anthropic::Client.new MODEL = "claude-opus-5" EFFORT = :xhigh SYSTEM_PROMPT = "You are a helpful general-purpose agent. Answer the user's request directly." REQUEST_TIMEOUT_SECONDS = 600 BASH_TIMEOUT_SECONDS = 60 TOOL_RESULT_MAX_CHARS = 8000 MAX_CONCURRENT = 10 DOC_TEST_MODE = !ENV["DOC_TEST_MODE"].to_s.empty? MAX_TOTAL_SUBTASKS = DOC_TEST_MODE ? 2 : 200 MAX_SUBAGENT_TURNS = DOC_TEST_MODE ? 1 : 15 MAX_MAIN_TURNS = DOC_TEST_MODE ? 1 : 30 TURNS_BETWEEN_REFRESHERS = 10 JOURNAL_PATH = ENV["ORCH_JOURNAL"].to_s.empty? ? "orchestration_journal.json" : ENV["ORCH_JOURNAL"] ``` ## Define the mode reminders The reminders are short on purpose. They flip the mode and point at the tool description, where the heavyweight instructions live. The full text is sent once when the mode turns on, the refresher is re-sent only after several user turns, and the exit notice is sent once when the mode turns off. ```python Python MODE_ENTER = ( "Orchestration mode is on: optimize for the most exhaustive, correct answer rather than " "the fastest one. Use the Workflow tool on every substantive task, sized to the problem's " "natural decomposition rather than the maximum the tool allows. See the Workflow tool's " "description for standing consent, granularity guidance, and quality patterns. Work solo " "only on conversational or trivial turns." ) MODE_REFRESH = ( "Orchestration mode is still on. Use the Workflow tool; see its standing consent section." ) MODE_EXIT = ( "Orchestration mode is off. The Workflow tool's standard opt-in rule applies again." ) ``` ```typescript TypeScript const MODE_ENTER = "Orchestration mode is on: optimize for the most exhaustive, correct answer rather than " + "the fastest one. Use the Workflow tool on every substantive task, sized to the problem's " + "natural decomposition rather than the maximum the tool allows. See the Workflow tool's " + "description for standing consent, granularity guidance, and quality patterns. Work solo " + "only on conversational or trivial turns."; const MODE_REFRESH = "Orchestration mode is still on. Use the Workflow tool; see its standing consent section."; const MODE_EXIT = "Orchestration mode is off. The Workflow tool's standard opt-in rule applies again."; ``` ```csharp C# const string modeEnter = "Orchestration mode is on: optimize for the most exhaustive, correct answer rather than " + "the fastest one. Use the Workflow tool on every substantive task, sized to the problem's " + "natural decomposition rather than the maximum the tool allows. See the Workflow tool's " + "description for standing consent, granularity guidance, and quality patterns. Work solo " + "only on conversational or trivial turns."; const string modeRefresh = "Orchestration mode is still on. Use the Workflow tool; see its standing consent section."; const string modeExit = "Orchestration mode is off. The Workflow tool's standard opt-in rule applies again."; ``` ```go Go const ( modeEnter = "Orchestration mode is on: optimize for the most exhaustive, correct answer rather than " + "the fastest one. Use the Workflow tool on every substantive task, sized to the problem's " + "natural decomposition rather than the maximum the tool allows. See the Workflow tool's " + "description for standing consent, granularity guidance, and quality patterns. Work solo " + "only on conversational or trivial turns." modeRefresh = "Orchestration mode is still on. Use the Workflow tool; see its standing consent section." modeExit = "Orchestration mode is off. The Workflow tool's standard opt-in rule applies again." ) ``` ```java Java static final String MODE_ENTER = "Orchestration mode is on: optimize for the most exhaustive, correct answer rather than " + "the fastest one. Use the Workflow tool on every substantive task, sized to the problem's " + "natural decomposition rather than the maximum the tool allows. See the Workflow tool's " + "description for standing consent, granularity guidance, and quality patterns. Work solo " + "only on conversational or trivial turns."; static final String MODE_REFRESH = "Orchestration mode is still on. Use the Workflow tool; see its standing consent section."; static final String MODE_EXIT = "Orchestration mode is off. The Workflow tool's standard opt-in rule applies again."; ``` ```php PHP const MODE_ENTER = 'Orchestration mode is on: optimize for the most exhaustive, correct answer rather than ' . 'the fastest one. Use the Workflow tool on every substantive task, sized to the problem\'s ' . 'natural decomposition rather than the maximum the tool allows. See the Workflow tool\'s ' . 'description for standing consent, granularity guidance, and quality patterns. Work solo ' . 'only on conversational or trivial turns.'; const MODE_REFRESH = 'Orchestration mode is still on. Use the Workflow tool; see its standing consent section.'; const MODE_EXIT = 'Orchestration mode is off. The Workflow tool\'s standard opt-in rule applies again.'; ``` ```ruby Ruby MODE_ENTER = "Orchestration mode is on: optimize for the most exhaustive, correct answer rather than " \ "the fastest one. Use the Workflow tool on every substantive task, sized to the problem's " \ "natural decomposition rather than the maximum the tool allows. See the Workflow tool's " \ "description for standing consent, granularity guidance, and quality patterns. Work solo " \ "only on conversational or trivial turns." MODE_REFRESH = "Orchestration mode is still on. Use the Workflow tool; see its standing consent section." MODE_EXIT = "Orchestration mode is off. The Workflow tool's standard opt-in rule applies again." ``` ## Grant standing consent in the tool description The Workflow tool carries the real behavioral contract: the opt-in rule, the standing consent that applies while the mode is on, granularity guidance for sizing the fan-out, and the quality patterns the model can reach for (a verification wave, a completeness critic, multiphase sequencing). Subagents also get a `report_findings` tool so their results come back as structured JSON instead of prose, and the bash tool is the Anthropic-defined `bash_20250124` tool run locally. ```python Python WORKFLOW_TOOL = { "name": "Workflow", "description": ( "Orchestrate a multiagent workflow: split a large task into independent subtasks " "and run them as parallel agents, then collect their results.\n\n" "Opt-in: only use this tool when the user explicitly asks for a workflow, or when a " "system message confirms that orchestration mode is on.\n\n" "Quality patterns: adversarial verification (a second wave of agents checks the first " "wave's findings against the source), a completeness critic (one agent hunts for what " "the others missed), and multiphase sequencing (understand, design, implement, and " "review as separate workflow calls, reading results between phases). A useful default " "is hybrid: scout inline first to discover the work-list, then fan out over it.\n\n" "Granularity: scope each subtask to a distinct concern, component, or question rather " "than per line or per file section. Scale the count to what the user asked for: a " "focused review of a module of a few hundred lines rarely needs more than about ten " "subtasks; a broad audit of a large codebase can justify more.\n\n" "Standing consent: while a system message confirms orchestration mode is on, that " "opt-in is standing. Author and run a workflow for every substantive task by default, " "and lean toward verifying findings adversarially. Work solo only on conversational " "turns or trivial mechanical edits. When a system message says the mode is off, " "revert to the opt-in rule above." ), "input_schema": { "type": "object", "properties": { "subtasks": { "type": "array", "items": {"type": "string"}, "description": "Independent subtask prompts to run as parallel agents", } }, "required": ["subtasks"], }, } BASH_TOOL = {"type": "bash_20250124", "name": "bash"} REPORT_TOOL = { "name": "report_findings", "description": ( "Report the final findings for your subtask. Call this exactly once, when you are " "done investigating; it ends your task." ), "input_schema": { "type": "object", "properties": { "summary": {"type": "string", "description": "Two or three sentences of synthesis"}, "findings": { "type": "array", "items": { "type": "object", "properties": { "claim": {"type": "string", "description": "The finding, one sentence"}, "evidence": { "type": "string", "description": "How it was verified (file, line, or command output)", }, "severity": {"type": "string", "enum": ["high", "medium", "low", "info"]}, }, "required": ["claim", "evidence", "severity"], }, }, }, "required": ["summary", "findings"], }, } ``` ```typescript TypeScript const WORKFLOW_TOOL: Anthropic.Tool = { name: "Workflow", description: "Orchestrate a multiagent workflow: split a large task into independent subtasks " + "and run them as parallel agents, then collect their results.\n\n" + "Opt-in: only use this tool when the user explicitly asks for a workflow, or when a " + "system message confirms that orchestration mode is on.\n\n" + "Quality patterns: adversarial verification (a second wave of agents checks the first " + "wave's findings against the source), a completeness critic (one agent hunts for what " + "the others missed), and multiphase sequencing (understand, design, implement, and " + "review as separate workflow calls, reading results between phases). A useful default " + "is hybrid: scout inline first to discover the work-list, then fan out over it.\n\n" + "Granularity: scope each subtask to a distinct concern, component, or question rather " + "than per line or per file section. Scale the count to what the user asked for: a " + "focused review of a module of a few hundred lines rarely needs more than about ten " + "subtasks; a broad audit of a large codebase can justify more.\n\n" + "Standing consent: while a system message confirms orchestration mode is on, that " + "opt-in is standing. Author and run a workflow for every substantive task by default, " + "and lean toward verifying findings adversarially. Work solo only on conversational " + "turns or trivial mechanical edits. When a system message says the mode is off, " + "revert to the opt-in rule above.", input_schema: { type: "object", properties: { subtasks: { type: "array", items: { type: "string" }, description: "Independent subtask prompts to run as parallel agents", }, }, required: ["subtasks"], }, }; const BASH_TOOL: Anthropic.ToolBash20250124 = { type: "bash_20250124", name: "bash" }; const REPORT_TOOL: Anthropic.Tool = { name: "report_findings", description: "Report the final findings for your subtask. Call this exactly once, when you are " + "done investigating; it ends your task.", input_schema: { type: "object", properties: { summary: { type: "string", description: "Two or three sentences of synthesis" }, findings: { type: "array", items: { type: "object", properties: { claim: { type: "string", description: "The finding, one sentence" }, evidence: { type: "string", description: "How it was verified (file, line, or command output)", }, severity: { type: "string", enum: ["high", "medium", "low", "info"] }, }, required: ["claim", "evidence", "severity"], }, }, }, required: ["summary", "findings"], }, }; ``` ```csharp C# Tool workflowTool = new() { Name = "Workflow", Description = "Orchestrate a multiagent workflow: split a large task into independent subtasks " + "and run them as parallel agents, then collect their results.\n\n" + "Opt-in: only use this tool when the user explicitly asks for a workflow, or when a " + "system message confirms that orchestration mode is on.\n\n" + "Quality patterns: adversarial verification (a second wave of agents checks the first " + "wave's findings against the source), a completeness critic (one agent hunts for what " + "the others missed), and multiphase sequencing (understand, design, implement, and " + "review as separate workflow calls, reading results between phases). A useful default " + "is hybrid: scout inline first to discover the work-list, then fan out over it.\n\n" + "Granularity: scope each subtask to a distinct concern, component, or question rather " + "than per line or per file section. Scale the count to what the user asked for: a " + "focused review of a module of a few hundred lines rarely needs more than about ten " + "subtasks; a broad audit of a large codebase can justify more.\n\n" + "Standing consent: while a system message confirms orchestration mode is on, that " + "opt-in is standing. Author and run a workflow for every substantive task by default, " + "and lean toward verifying findings adversarially. Work solo only on conversational " + "turns or trivial mechanical edits. When a system message says the mode is off, " + "revert to the opt-in rule above.", InputSchema = new InputSchema { Properties = new Dictionary { ["subtasks"] = JsonSerializer.SerializeToElement(new { type = "array", items = new { type = "string" }, description = "Independent subtask prompts to run as parallel agents", }), }, Required = ["subtasks"], }, }; ToolBash20250124 bashTool = new(); Tool reportTool = new() { Name = "report_findings", Description = "Report the final findings for your subtask. Call this exactly once, when you are " + "done investigating; it ends your task.", InputSchema = new InputSchema { Properties = new Dictionary { ["summary"] = JsonSerializer.SerializeToElement(new { type = "string", description = "Two or three sentences of synthesis", }), ["findings"] = JsonSerializer.SerializeToElement(new { type = "array", items = new { type = "object", properties = new { claim = new { type = "string", description = "The finding, one sentence" }, evidence = new { type = "string", description = "How it was verified (file, line, or command output)", }, severity = new { type = "string", @enum = new[] { "high", "medium", "low", "info" } }, }, required = new[] { "claim", "evidence", "severity" }, }, }), }, Required = ["summary", "findings"], }, }; ``` ```go Go var workflowTool = anthropic.ToolUnionParam{ OfTool: &anthropic.ToolParam{ Name: "Workflow", Description: anthropic.String("Orchestrate a multiagent workflow: split a large task into independent subtasks " + "and run them as parallel agents, then collect their results.\n\n" + "Opt-in: only use this tool when the user explicitly asks for a workflow, or when a " + "system message confirms that orchestration mode is on.\n\n" + "Quality patterns: adversarial verification (a second wave of agents checks the first " + "wave's findings against the source), a completeness critic (one agent hunts for what " + "the others missed), and multiphase sequencing (understand, design, implement, and " + "review as separate workflow calls, reading results between phases). A useful default " + "is hybrid: scout inline first to discover the work-list, then fan out over it.\n\n" + "Granularity: scope each subtask to a distinct concern, component, or question rather " + "than per line or per file section. Scale the count to what the user asked for: a " + "focused review of a module of a few hundred lines rarely needs more than about ten " + "subtasks; a broad audit of a large codebase can justify more.\n\n" + "Standing consent: while a system message confirms orchestration mode is on, that " + "opt-in is standing. Author and run a workflow for every substantive task by default, " + "and lean toward verifying findings adversarially. Work solo only on conversational " + "turns or trivial mechanical edits. When a system message says the mode is off, " + "revert to the opt-in rule above."), InputSchema: anthropic.ToolInputSchemaParam{ Properties: map[string]any{ "subtasks": map[string]any{ "type": "array", "items": map[string]any{"type": "string"}, "description": "Independent subtask prompts to run as parallel agents", }, }, Required: []string{"subtasks"}, }, }, } var bashTool = anthropic.ToolUnionParam{ OfBashTool20250124: &anthropic.ToolBash20250124Param{}, } var reportTool = anthropic.ToolUnionParam{ OfTool: &anthropic.ToolParam{ Name: "report_findings", Description: anthropic.String("Report the final findings for your subtask. Call this exactly once, when you are " + "done investigating; it ends your task."), InputSchema: anthropic.ToolInputSchemaParam{ Properties: map[string]any{ "summary": map[string]any{"type": "string", "description": "Two or three sentences of synthesis"}, "findings": map[string]any{ "type": "array", "items": map[string]any{ "type": "object", "properties": map[string]any{ "claim": map[string]any{"type": "string", "description": "The finding, one sentence"}, "evidence": map[string]any{ "type": "string", "description": "How it was verified (file, line, or command output)", }, "severity": map[string]any{"type": "string", "enum": []string{"high", "medium", "low", "info"}}, }, "required": []string{"claim", "evidence", "severity"}, }, }, }, Required: []string{"summary", "findings"}, }, }, } ``` ```java Java static final Tool WORKFLOW_TOOL = Tool.builder() .name("Workflow") .description("Orchestrate a multiagent workflow: split a large task into independent subtasks " + "and run them as parallel agents, then collect their results.\n\n" + "Opt-in: only use this tool when the user explicitly asks for a workflow, or when a " + "system message confirms that orchestration mode is on.\n\n" + "Quality patterns: adversarial verification (a second wave of agents checks the first " + "wave's findings against the source), a completeness critic (one agent hunts for what " + "the others missed), and multiphase sequencing (understand, design, implement, and " + "review as separate workflow calls, reading results between phases). A useful default " + "is hybrid: scout inline first to discover the work-list, then fan out over it.\n\n" + "Granularity: scope each subtask to a distinct concern, component, or question rather " + "than per line or per file section. Scale the count to what the user asked for: a " + "focused review of a module of a few hundred lines rarely needs more than about ten " + "subtasks; a broad audit of a large codebase can justify more.\n\n" + "Standing consent: while a system message confirms orchestration mode is on, that " + "opt-in is standing. Author and run a workflow for every substantive task by default, " + "and lean toward verifying findings adversarially. Work solo only on conversational " + "turns or trivial mechanical edits. When a system message says the mode is off, " + "revert to the opt-in rule above.") .inputSchema(Tool.InputSchema.builder() .properties(JsonValue.from(Map.of( "subtasks", Map.of( "type", "array", "items", Map.of("type", "string"), "description", "Independent subtask prompts to run as parallel agents")))) .putAdditionalProperty("required", JsonValue.from(List.of("subtasks"))) .build()) .build(); static final ToolBash20250124 BASH_TOOL = ToolBash20250124.builder().build(); static final Tool REPORT_TOOL = Tool.builder() .name("report_findings") .description("Report the final findings for your subtask. Call this exactly once, when you are " + "done investigating; it ends your task.") .inputSchema(Tool.InputSchema.builder() .properties(JsonValue.from(Map.of( "summary", Map.of("type", "string", "description", "Two or three sentences of synthesis"), "findings", Map.of( "type", "array", "items", Map.of( "type", "object", "properties", Map.of( "claim", Map.of( "type", "string", "description", "The finding, one sentence"), "evidence", Map.of( "type", "string", "description", "How it was verified (file, line, or command output)"), "severity", Map.of( "type", "string", "enum", List.of("high", "medium", "low", "info"))), "required", List.of("claim", "evidence", "severity")))))) .putAdditionalProperty("required", JsonValue.from(List.of("summary", "findings"))) .build()) .build(); ``` ```php PHP const WORKFLOW_TOOL = [ 'name' => 'Workflow', 'description' => 'Orchestrate a multiagent workflow: split a large task into independent subtasks ' . "and run them as parallel agents, then collect their results.\n\n" . 'Opt-in: only use this tool when the user explicitly asks for a workflow, or when a ' . "system message confirms that orchestration mode is on.\n\n" . 'Quality patterns: adversarial verification (a second wave of agents checks the first ' . 'wave\'s findings against the source), a completeness critic (one agent hunts for what ' . 'the others missed), and multiphase sequencing (understand, design, implement, and ' . 'review as separate workflow calls, reading results between phases). A useful default ' . "is hybrid: scout inline first to discover the work-list, then fan out over it.\n\n" . 'Granularity: scope each subtask to a distinct concern, component, or question rather ' . 'than per line or per file section. Scale the count to what the user asked for: a ' . 'focused review of a module of a few hundred lines rarely needs more than about ten ' . "subtasks; a broad audit of a large codebase can justify more.\n\n" . 'Standing consent: while a system message confirms orchestration mode is on, that ' . 'opt-in is standing. Author and run a workflow for every substantive task by default, ' . 'and lean toward verifying findings adversarially. Work solo only on conversational ' . 'turns or trivial mechanical edits. When a system message says the mode is off, ' . 'revert to the opt-in rule above.', 'input_schema' => [ 'type' => 'object', 'properties' => [ 'subtasks' => [ 'type' => 'array', 'items' => ['type' => 'string'], 'description' => 'Independent subtask prompts to run as parallel agents', ], ], 'required' => ['subtasks'], ], ]; const BASH_TOOL = ['type' => 'bash_20250124', 'name' => 'bash']; const REPORT_TOOL = [ 'name' => 'report_findings', 'description' => 'Report the final findings for your subtask. Call this exactly once, when you are ' . 'done investigating; it ends your task.', 'input_schema' => [ 'type' => 'object', 'properties' => [ 'summary' => ['type' => 'string', 'description' => 'Two or three sentences of synthesis'], 'findings' => [ 'type' => 'array', 'items' => [ 'type' => 'object', 'properties' => [ 'claim' => ['type' => 'string', 'description' => 'The finding, one sentence'], 'evidence' => [ 'type' => 'string', 'description' => 'How it was verified (file, line, or command output)', ], 'severity' => ['type' => 'string', 'enum' => ['high', 'medium', 'low', 'info']], ], 'required' => ['claim', 'evidence', 'severity'], ], ], ], 'required' => ['summary', 'findings'], ], ]; ``` ```ruby Ruby WORKFLOW_TOOL = { name: "Workflow", description: "Orchestrate a multiagent workflow: split a large task into independent subtasks " \ "and run them as parallel agents, then collect their results.\n\n" \ "Opt-in: only use this tool when the user explicitly asks for a workflow, or when a " \ "system message confirms that orchestration mode is on.\n\n" \ "Quality patterns: adversarial verification (a second wave of agents checks the first " \ "wave's findings against the source), a completeness critic (one agent hunts for what " \ "the others missed), and multiphase sequencing (understand, design, implement, and " \ "review as separate workflow calls, reading results between phases). A useful default " \ "is hybrid: scout inline first to discover the work-list, then fan out over it.\n\n" \ "Granularity: scope each subtask to a distinct concern, component, or question rather " \ "than per line or per file section. Scale the count to what the user asked for: a " \ "focused review of a module of a few hundred lines rarely needs more than about ten " \ "subtasks; a broad audit of a large codebase can justify more.\n\n" \ "Standing consent: while a system message confirms orchestration mode is on, that " \ "opt-in is standing. Author and run a workflow for every substantive task by default, " \ "and lean toward verifying findings adversarially. Work solo only on conversational " \ "turns or trivial mechanical edits. When a system message says the mode is off, " \ "revert to the opt-in rule above.", input_schema: { type: "object", properties: { subtasks: { type: "array", items: {type: "string"}, description: "Independent subtask prompts to run as parallel agents" } }, required: ["subtasks"] } }.freeze BASH_TOOL = {type: "bash_20250124", name: "bash"}.freeze REPORT_TOOL = { name: "report_findings", description: "Report the final findings for your subtask. Call this exactly once, when you are " \ "done investigating; it ends your task.", input_schema: { type: "object", properties: { summary: {type: "string", description: "Two or three sentences of synthesis"}, findings: { type: "array", items: { type: "object", properties: { claim: {type: "string", description: "The finding, one sentence"}, evidence: { type: "string", description: "How it was verified (file, line, or command output)" }, severity: {type: "string", enum: ["high", "medium", "low", "info"]} }, required: ["claim", "evidence", "severity"] } } }, required: ["summary", "findings"] } }.freeze ``` ## Run the bash tool locally The bash handler runs the requested command with a timeout, captures combined stdout and stderr, and truncates the result so a runaway command can't flood the context window. Commands run in the directory you launch the example from, so pointing it at a project means starting it there; when `DOC_TEST_MODE` is set, the harness instead gives bash a small throwaway fixture directory that is removed on exit. There is no sandbox here: the command runs with the permissions of the process that launched the example. For clarity this example runs each call in a fresh subshell rather than maintaining the persistent session the `bash_20250124` contract describes; a production agent should back the tool with a long-lived shell so that working directory, environment, and the `restart` action behave as documented. ```python Python # Run bash where the example was launched. In DOC_TEST_MODE the docs harness # points it at a throwaway fixture directory instead, removed on exit. if DOC_TEST_MODE: WORK_DIR = tempfile.mkdtemp(prefix="orchestration-") atexit.register(shutil.rmtree, WORK_DIR, ignore_errors=True) with open(os.path.join(WORK_DIR, "sample.py"), "w") as fixture: fixture.write( "def fib(n):\n" " return n if n < 2 else fib(n - 1) + fib(n - 2)\n\n" "print(fib(10))\n" ) else: WORK_DIR = os.getcwd() def run_bash(command: str) -> tuple[str, bool]: """Run a shell command and return (output, is_error). No sandbox: example code only.""" print(f"[bash] {command}", file=sys.stderr) try: proc = subprocess.run( ["bash", "-c", command], cwd=WORK_DIR, capture_output=True, text=True, errors="replace", timeout=BASH_TIMEOUT_SECONDS, ) except subprocess.TimeoutExpired: return f"command timed out after {BASH_TIMEOUT_SECONDS}s", True output = (proc.stdout + proc.stderr).strip() or "(no output)" if len(output) > TOOL_RESULT_MAX_CHARS: output = output[:TOOL_RESULT_MAX_CHARS] + f"\n(truncated at {TOOL_RESULT_MAX_CHARS} chars)" if proc.returncode != 0: output = f"(exit code {proc.returncode})\n{output}" return output, proc.returncode != 0 def handle_bash_block(block) -> tuple[str, bool]: if block.input.get("restart") is True: return "Shell restarted.", False command = block.input.get("command") if not isinstance(command, str) or not command: return "bash error: no command was provided.", True return run_bash(command) ``` ```typescript TypeScript const execShell = promisify(exec); // Run bash where the example was launched. In DOC_TEST_MODE the docs harness // points it at a throwaway fixture directory instead, removed on exit. const WORK_DIR = DOC_TEST_MODE ? await mkdtemp(join(tmpdir(), "orchestration-")) : process.cwd(); if (DOC_TEST_MODE) { await writeFile( join(WORK_DIR, "sample.py"), "def fib(n):\n" + " return n if n < 2 else fib(n - 1) + fib(n - 2)\n\n" + "print(fib(10))\n", ); process.on("exit", () => rmSync(WORK_DIR, { recursive: true, force: true })); } // Run a shell command and return its output. No sandbox: example code only. async function runBash(command: string): Promise<{ output: string; isError: boolean }> { console.error(`[bash] ${command}`); let stdout = ""; let stderr = ""; let exitCode = 0; try { ({ stdout, stderr } = await execShell(command, { shell: "/bin/bash", cwd: WORK_DIR, timeout: BASH_TIMEOUT_SECONDS * 1000, maxBuffer: 16 * 1024 * 1024, })); } catch (error) { const failure = error as { stdout?: string; stderr?: string; code?: number | string; killed?: boolean; }; if (failure.killed && failure.code !== "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") { return { output: `command timed out after ${BASH_TIMEOUT_SECONDS}s`, isError: true }; } stdout = failure.stdout ?? ""; stderr = failure.stderr ?? ""; exitCode = typeof failure.code === "number" ? failure.code : 1; } let output = (stdout + stderr).trim() || "(no output)"; const codePoints = [...output]; if (codePoints.length > TOOL_RESULT_MAX_CHARS) { output = codePoints.slice(0, TOOL_RESULT_MAX_CHARS).join("") + `\n(truncated at ${TOOL_RESULT_MAX_CHARS} chars)`; } if (exitCode !== 0) { output = `(exit code ${exitCode})\n${output}`; } return { output, isError: exitCode !== 0 }; } async function handleBashBlock( block: Anthropic.ToolUseBlock, ): Promise<{ output: string; isError: boolean }> { const input = block.input as { command?: string; restart?: boolean }; if (input.restart === true) { return { output: "Shell restarted.", isError: false }; } if (!input.command) { return { output: "bash error: no command was provided.", isError: true }; } return runBash(input.command); } ``` ```csharp C# // Run bash where the example was launched. In DOC_TEST_MODE the docs harness // points it at a throwaway fixture directory instead, removed on exit. var workDir = Environment.CurrentDirectory; if (docTestMode) { workDir = Directory.CreateTempSubdirectory("orchestration-").FullName; File.WriteAllText(Path.Combine(workDir, "sample.py"), "def fib(n):\n" + " return n if n < 2 else fib(n - 1) + fib(n - 2)\n\n" + "print(fib(10))\n"); var fixtureDir = workDir; AppDomain.CurrentDomain.ProcessExit += (_, _) => { try { Directory.Delete(fixtureDir, recursive: true); } catch { /* Best-effort cleanup; the OS tmp sweeper handles leftovers. */ } }; } // Run a shell command and return its output plus an error flag. No sandbox: example code only. async Task<(string Output, bool IsError)> RunBash(string command) { Console.Error.WriteLine($"[bash] {command}"); using var process = Process.Start(new ProcessStartInfo("bash") { ArgumentList = { "-c", command }, WorkingDirectory = workDir, RedirectStandardOutput = true, RedirectStandardError = true, }); if (process is null) { return ("bash error: the shell process failed to start.", true); } var stdoutTask = process.StandardOutput.ReadToEndAsync(); var stderrTask = process.StandardError.ReadToEndAsync(); using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(bashTimeoutSeconds)); try { await process.WaitForExitAsync(timeout.Token); } catch (OperationCanceledException) { process.Kill(entireProcessTree: true); // Let the reader tasks finish before the process is disposed. try { await Task.WhenAll(stdoutTask, stderrTask); } catch { // The output is discarded on timeout, so reader failures are ignored too. } return ($"command timed out after {bashTimeoutSeconds}s", true); } var output = (await stdoutTask + await stderrTask).Trim(); if (output.Length == 0) { output = "(no output)"; } if (output.Length > toolResultMaxChars) { output = output[..toolResultMaxChars] + $"\n(truncated at {toolResultMaxChars} chars)"; } if (process.ExitCode != 0) { output = $"(exit code {process.ExitCode})\n{output}"; } return (output, process.ExitCode != 0); } // Execute one bash tool call requested by the model. async Task<(string Output, bool IsError)> HandleBashBlock(ToolUseBlock block) { if (block.Input.TryGetValue("restart", out var restart) && restart.ValueKind == JsonValueKind.True) { return ("Shell restarted.", false); } var command = block.Input.TryGetValue("command", out var rawCommand) && rawCommand.ValueKind == JsonValueKind.String ? rawCommand.GetString()! : ""; if (command.Length == 0) { return ("bash error: no command was provided.", true); } return await RunBash(command); } ``` ```go Go // Run bash where the example was launched. In DOC_TEST_MODE the docs harness // points it at a throwaway fixture directory instead, removed on exit. var workDir = func() string { if !docTestMode { dir, err := os.Getwd() if err != nil { log.Fatal(err) } return dir } dir, err := os.MkdirTemp("", "orchestration-") if err != nil { log.Fatal(err) } fixture := "def fib(n):\n" + " return n if n < 2 else fib(n - 1) + fib(n - 2)\n\n" + "print(fib(10))\n" if err := os.WriteFile(filepath.Join(dir, "sample.py"), []byte(fixture), 0o644); err != nil { log.Fatal(err) } return dir }() // runBash runs a shell command and returns its output plus an error flag. // No sandbox: example code only. func runBash(ctx context.Context, command string) (string, bool) { fmt.Fprintf(os.Stderr, "[bash] %s\n", command) ctx, cancel := context.WithTimeout(ctx, bashTimeoutSeconds*time.Second) defer cancel() cmd := exec.CommandContext(ctx, "bash", "-c", command) cmd.Dir = workDir combined, err := cmd.CombinedOutput() if errors.Is(ctx.Err(), context.DeadlineExceeded) { return fmt.Sprintf("command timed out after %ds", bashTimeoutSeconds), true } output := strings.TrimSpace(string(combined)) if output == "" { output = "(no output)" } if runes := []rune(output); len(runes) > toolResultMaxChars { output = string(runes[:toolResultMaxChars]) + fmt.Sprintf("\n(truncated at %d chars)", toolResultMaxChars) } if err == nil { return output, false } var exitErr *exec.ExitError if errors.As(err, &exitErr) { return fmt.Sprintf("(exit code %d)\n%s", exitErr.ExitCode(), output), true } return fmt.Sprintf("(%s)\n%s", err, output), true } // handleBashBlock executes one bash tool call requested by the model. func handleBashBlock(ctx context.Context, block anthropic.ToolUseBlock) (string, bool) { var input struct { Command string `json:"command"` Restart bool `json:"restart"` } if err := json.Unmarshal(block.Input, &input); err != nil { return fmt.Sprintf("bash error: could not parse input: %s", err), true } if input.Restart { return "Shell restarted.", false } if input.Command == "" { return "bash error: no command was provided.", true } return runBash(ctx, input.Command) } ``` ```java Java record ToolOutput(String output, boolean isError) {} // Run bash where the example was launched. In DOC_TEST_MODE the docs harness // points it at a throwaway fixture directory instead, removed on exit. static final Path WORK_DIR = createWorkDir(); static Path createWorkDir() { if (!DOC_TEST_MODE) { return Path.of(System.getProperty("user.dir")); } try { var dir = Files.createTempDirectory("orchestration-"); Files.writeString(dir.resolve("sample.py"), """ def fib(n): return n if n < 2 else fib(n - 1) + fib(n - 2) print(fib(10)) """); Runtime.getRuntime().addShutdownHook(new Thread(() -> { try (var paths = Files.walk(dir)) { paths.sorted(Comparator.reverseOrder()).forEach(p -> { try { Files.deleteIfExists(p); } catch (IOException ignored) {} }); } catch (IOException ignored) { // Best-effort cleanup; the OS tmp sweeper handles leftovers. } })); return dir; } catch (IOException error) { throw new UncheckedIOException(error); } } // Run a shell command and return its output plus an error flag. No sandbox: example code only. ToolOutput runBash(String command) throws InterruptedException { System.err.println("[bash] " + command); Process process; try { process = new ProcessBuilder("bash", "-c", command) .directory(WORK_DIR.toFile()) .redirectErrorStream(true) .start(); } catch (IOException error) { return new ToolOutput("(" + error + ")", true); } // Drain stdout on another thread so a filled pipe cannot stall the timeout wait below. CompletableFuture outputReader = CompletableFuture.supplyAsync(() -> { try (var stdout = process.getInputStream()) { return new String(stdout.readAllBytes(), StandardCharsets.UTF_8); } catch (IOException error) { return ""; } }); if (!process.waitFor(BASH_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { process.destroyForcibly(); outputReader.cancel(true); return new ToolOutput("command timed out after " + BASH_TIMEOUT_SECONDS + "s", true); } String output = outputReader.join().trim(); if (output.isEmpty()) { output = "(no output)"; } if (output.length() > TOOL_RESULT_MAX_CHARS) { output = output.substring(0, TOOL_RESULT_MAX_CHARS) + "\n(truncated at " + TOOL_RESULT_MAX_CHARS + " chars)"; } int exitCode = process.exitValue(); if (exitCode != 0) { return new ToolOutput("(exit code " + exitCode + ")\n" + output, true); } return new ToolOutput(output, false); } // Execute one bash tool call requested by the model. ToolOutput handleBashBlock(ToolUseBlock block) throws InterruptedException { Map input = (Map) block._input().asObject().orElse(Map.of()); JsonValue restart = input.getOrDefault("restart", JsonValue.from(false)); if (Boolean.TRUE.equals(restart.asBoolean().orElse(false))) { return new ToolOutput("Shell restarted.", false); } JsonValue raw = input.get("command"); String command = raw != null && raw.asString().isPresent() ? raw.asStringOrThrow() : ""; if (command.isEmpty()) { return new ToolOutput("bash error: no command was provided.", true); } return runBash(command); } ``` ```php PHP // Run bash where the example was launched. In DOC_TEST_MODE the docs harness // points it at a throwaway fixture directory instead, removed on exit. if (DOC_TEST_MODE) { $workDir = sys_get_temp_dir() . '/orchestration-' . bin2hex(random_bytes(8)); if (!mkdir($workDir, 0700)) { throw new RuntimeException("could not create working directory {$workDir}"); } file_put_contents( $workDir . '/sample.py', "def fib(n):\n" . " return n if n < 2 else fib(n - 1) + fib(n - 2)\n\n" . "print(fib(10))\n", ); register_shutdown_function(function () use ($workDir): void { foreach (glob($workDir . '/*') ?: [] as $entry) { @unlink($entry); } @rmdir($workDir); }); } else { $workDir = getcwd() ?: '.'; } define('WORK_DIR', $workDir); /** * Run a shell command and return [output, isError]. The coreutils timeout command * enforces the time limit. No sandbox: example code only. */ function runBash(string $command): array { fwrite(STDERR, "[bash] {$command}\n"); // Requires GNU coreutils 'timeout'. On macOS: brew install coreutils, or replace with gtimeout. exec( 'cd ' . escapeshellarg(WORK_DIR) . ' && timeout ' . BASH_TIMEOUT_SECONDS . ' bash -c ' . escapeshellarg($command) . ' 2>&1', $outputLines, $exitCode, ); if ($exitCode === 124) { return ['command timed out after ' . BASH_TIMEOUT_SECONDS . 's', true]; } $output = trim(implode("\n", $outputLines)); if ($output === '') { $output = '(no output)'; } if (mb_strlen($output) > TOOL_RESULT_MAX_CHARS) { $output = mb_substr($output, 0, TOOL_RESULT_MAX_CHARS) . "\n(truncated at " . TOOL_RESULT_MAX_CHARS . ' chars)'; } if ($exitCode !== 0) { $output = "(exit code {$exitCode})\n{$output}"; } return [$output, $exitCode !== 0]; } /** Execute one bash tool call requested by the model. */ function handleBashBlock(ToolUseBlock $block): array { if (($block->input['restart'] ?? null) === true) { return ['Shell restarted.', false]; } $command = $block->input['command'] ?? ''; if (!is_string($command) || $command === '') { return ['bash error: no command was provided.', true]; } return runBash($command); } ``` ```ruby Ruby # Run bash where the example was launched. In DOC_TEST_MODE the docs harness # points it at a throwaway fixture directory instead, removed on exit. WORK_DIR = if DOC_TEST_MODE Dir.mktmpdir("orchestration-").tap do |dir| File.write(File.join(dir, "sample.py"), <<~PYTHON) def fib(n): return n if n < 2 else fib(n - 1) + fib(n - 2) print(fib(10)) PYTHON at_exit { FileUtils.remove_entry(dir, true) } end else Dir.pwd end # Tool input arrives as a Hash or as a raw JSON string from the streaming # accumulator; normalize either shape to a string-keyed Hash. def parse_tool_input(raw) return raw.transform_keys(&:to_s) if raw.is_a?(Hash) parsed = JSON.parse(raw.to_s) rescue nil parsed.is_a?(Hash) ? parsed : {} end # Run a shell command and return [output, is_error]. No sandbox: example code only. def run_bash(command) warn "[bash] #{command}" begin stdin, stdout_and_stderr, wait_thr = Open3.popen2e("bash", "-c", command, pgroup: true, chdir: WORK_DIR) stdin.close reader = Thread.new { stdout_and_stderr.read.scrub } # Enforce the time limit with a monotonic-clock deadline so a timed-out command is # terminated rather than left running in the background. deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + BASH_TIMEOUT_SECONDS until wait_thr.join(0.1) next if Process.clock_gettime(Process::CLOCK_MONOTONIC) < deadline begin Process.kill("-TERM", wait_thr.pid) rescue Errno::ESRCH end unless wait_thr.join(2) begin Process.kill("-KILL", wait_thr.pid) rescue Errno::ESRCH end end wait_thr.join(5) reader.join(1) || reader.kill stdout_and_stderr.close rescue nil return ["command timed out after #{BASH_TIMEOUT_SECONDS}s", true] end status = wait_thr.value output = reader.value.strip stdout_and_stderr.close output = "(no output)" if output.empty? if output.length > TOOL_RESULT_MAX_CHARS output = "#{output[0, TOOL_RESULT_MAX_CHARS]}\n(truncated at #{TOOL_RESULT_MAX_CHARS} chars)" end output = "(exit code #{status.exitstatus})\n#{output}" unless status.success? [output, !status.success?] rescue Errno::ENOENT => e return ["bash error: #{e.message}", true] end end # Execute one bash tool call requested by the model. def handle_bash_block(block) input = parse_tool_input(block.input) return ["Shell restarted.", false] if input["restart"] == true command = input["command"] return ["bash error: no command was provided.", true] unless command.is_a?(String) && !command.empty? run_bash(command) end # Convert response content to request-shaped params. The streaming accumulator # returns tool_use input as a raw JSON string and includes response-only fields, # so reshape each block to the request schema before echoing it back. def assistant_content_param(content) content.map do |block| case block.type when :tool_use input = parse_tool_input(block.input) {type: "tool_use", id: block.id, name: block.name, input: input} when :text {type: "text", text: block.text} when :thinking {type: "thinking", thinking: block.thinking, signature: block.signature} when :redacted_thinking then {type: "redacted_thinking", data: block.data} else block.to_h end end end ``` ## Run one subagent Each workflow subtask becomes its own small agent loop with the bash tool, running at the same effort as the main loop. A per-request timeout bounds each API call so a dropped connection degrades one subagent instead of stalling the whole run. ```python Python def run_subagent(model: str, prompt: str) -> str: """One subagent: a small nested agent loop with the bash tool plus report_findings. Subagents inherit the main loop's effort level.""" subagent_system = ( "You are one agent in a larger parallel fan-out, assigned a single subtask. " "Investigate it directly, using bash to check facts rather than guessing, and finish " "by calling report_findings exactly once. Return findings, not narration." ) messages = [{"role": "user", "content": prompt}] for _ in range(MAX_SUBAGENT_TURNS): with client.messages.stream( model=model, max_tokens=64000, system=subagent_system, output_config={"effort": EFFORT}, tools=[BASH_TOOL, REPORT_TOOL], messages=messages, timeout=REQUEST_TIMEOUT_SECONDS, ) as stream: response = stream.get_final_message() messages.append({"role": "assistant", "content": response.content}) if response.stop_reason == "pause_turn": continue if response.stop_reason != "tool_use": text = "".join(block.text for block in response.content if block.type == "text") if response.stop_reason == "max_tokens": text += "\n\n(warning: subagent response was truncated at max_tokens)" return text tool_results = [] report = None for block in response.content: if block.type != "tool_use": continue if block.name == "report_findings": report = json.dumps(block.input, indent=2) output, is_error = "Findings recorded.", False elif block.name == "bash": output, is_error = handle_bash_block(block) else: output, is_error = f"unknown tool: {block.name}", True tool_results.append( { "type": "tool_result", "tool_use_id": block.id, "content": output, "is_error": is_error, } ) if report is not None: return report messages.append({"role": "user", "content": tool_results}) return "(subagent hit the turn limit before finishing)" ``` ```typescript TypeScript // One subagent: a small nested agent loop with the bash tool plus report_findings. // Subagents inherit the main loop's effort level. async function runSubagent(model: string, prompt: string): Promise { const subagentSystem = "You are one agent in a larger parallel fan-out, assigned a single subtask. " + "Investigate it directly, using bash to check facts rather than guessing, and finish " + "by calling report_findings exactly once. Return findings, not narration."; const messages: Anthropic.MessageParam[] = [{ role: "user", content: prompt }]; for (let turn = 0; turn < MAX_SUBAGENT_TURNS; turn++) { const response = await client.messages .stream( { model, max_tokens: 64000, system: subagentSystem, output_config: { effort: EFFORT }, tools: [BASH_TOOL, REPORT_TOOL], messages, }, { signal: AbortSignal.timeout(REQUEST_TIMEOUT_SECONDS * 1000) }, ) .finalMessage(); messages.push({ role: "assistant", content: response.content }); if (response.stop_reason === "pause_turn") { continue; } if (response.stop_reason !== "tool_use") { let text = response.content .filter((block): block is Anthropic.TextBlock => block.type === "text") .map((block) => block.text) .join(""); if (response.stop_reason === "max_tokens") { text += "\n\n(warning: subagent response was truncated at max_tokens)"; } return text; } const toolResults: Anthropic.ToolResultBlockParam[] = []; let report: string | null = null; for (const block of response.content) { if (block.type !== "tool_use") { continue; } let output: string; let isError: boolean; if (block.name === "report_findings") { report = JSON.stringify(block.input, null, 2); output = "Findings recorded."; isError = false; } else if (block.name === "bash") { ({ output, isError } = await handleBashBlock(block)); } else { output = `unknown tool: ${block.name}`; isError = true; } toolResults.push({ type: "tool_result", tool_use_id: block.id, content: output, is_error: isError, }); } if (report !== null) { return report; } messages.push({ role: "user", content: toolResults }); } return "(subagent hit the turn limit before finishing)"; } ``` ```csharp C# // One subagent: a small nested agent loop with the bash tool plus report_findings. // Subagents inherit the main loop's effort level. async Task RunSubagent(string prompt) { const string subagentSystem = "You are one agent in a larger parallel fan-out, assigned a single subtask. " + "Investigate it directly, using bash to check facts rather than guessing, and finish " + "by calling report_findings exactly once. Return findings, not narration."; List messages = [new() { Role = Role.User, Content = prompt }]; for (var turn = 0; turn < maxSubagentTurns; turn++) { using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(requestTimeoutSeconds)); var response = await client.Messages.Create(new MessageCreateParams { Model = model, MaxTokens = requestMaxTokens, System = subagentSystem, OutputConfig = new OutputConfig { Effort = effort }, Tools = [bashTool, reportTool], Messages = messages, }, cancellationToken: deadline.Token); messages.Add(new() { Role = Role.Assistant, Content = response.Content.Select(block => new ContentBlockParam(block.Json)).ToList(), }); if (response.StopReason == StopReason.PauseTurn) { continue; } if (response.StopReason != StopReason.ToolUse) { var text = string.Concat( response.Content.Select(block => block.TryPickText(out var textBlock) ? textBlock.Text : "")); if (response.StopReason == StopReason.MaxTokens) { text += "\n\n(warning: subagent response was truncated at max_tokens)"; } return text; } List toolResults = []; string? report = null; foreach (var block in response.Content) { if (!block.TryPickToolUse(out var toolUse)) { continue; } string output; bool isError; if (toolUse.Name == "report_findings") { report = JsonSerializer.Serialize( toolUse.Input, new JsonSerializerOptions { WriteIndented = true }); output = "Findings recorded."; isError = false; } else if (toolUse.Name == "bash") { (output, isError) = await HandleBashBlock(toolUse); } else { output = $"unknown tool: {toolUse.Name}"; isError = true; } toolResults.Add(new ToolResultBlockParam(toolUse.ID) { Content = output, IsError = isError }); } if (report is not null) { return report; } messages.Add(new() { Role = Role.User, Content = toolResults }); } return "(subagent hit the turn limit before finishing)"; } ``` ```go Go // runSubagent runs one subagent: a small nested agent loop with the bash tool plus // report_findings. Subagents inherit the main loop's effort level. func runSubagent(ctx context.Context, model string, prompt string) (string, error) { subagentSystem := "You are one agent in a larger parallel fan-out, assigned a single subtask. " + "Investigate it directly, using bash to check facts rather than guessing, and finish " + "by calling report_findings exactly once. Return findings, not narration." messages := []anthropic.MessageParam{anthropic.NewUserMessage(anthropic.NewTextBlock(prompt))} for range maxSubagentTurns { var response anthropic.Message err := func() error { ctx, cancel := context.WithTimeout(ctx, requestTimeoutSeconds*time.Second) defer cancel() stream := client.Messages.NewStreaming(ctx, anthropic.MessageNewParams{ Model: model, MaxTokens: 64000, System: []anthropic.TextBlockParam{{Text: subagentSystem}}, OutputConfig: anthropic.OutputConfigParam{Effort: effort}, Tools: []anthropic.ToolUnionParam{bashTool, reportTool}, Messages: messages, }) defer stream.Close() for stream.Next() { if err := response.Accumulate(stream.Current()); err != nil { return err } } return stream.Err() }() if err != nil { return "", err } messages = append(messages, response.ToParam()) if response.StopReason == anthropic.StopReasonPauseTurn { continue } if response.StopReason != anthropic.StopReasonToolUse { var text strings.Builder for _, block := range response.Content { if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok { text.WriteString(textBlock.Text) } } if response.StopReason == anthropic.StopReasonMaxTokens { text.WriteString("\n\n(warning: subagent response was truncated at max_tokens)") } return text.String(), nil } var toolResults []anthropic.ContentBlockParamUnion var report string var reportRecorded bool for _, block := range response.Content { toolUse, ok := block.AsAny().(anthropic.ToolUseBlock) if !ok { continue } var output string var isError bool switch toolUse.Name { case "report_findings": report = string(toolUse.Input) var pretty bytes.Buffer if err := json.Indent(&pretty, toolUse.Input, "", " "); err == nil { report = pretty.String() } reportRecorded = true output = "Findings recorded." case "bash": output, isError = handleBashBlock(ctx, toolUse) default: output, isError = fmt.Sprintf("unknown tool: %s", toolUse.Name), true } toolResults = append(toolResults, anthropic.NewToolResultBlock(toolUse.ID, output, isError)) } if reportRecorded { return report, nil } messages = append(messages, anthropic.NewUserMessage(toolResults...)) } return "(subagent hit the turn limit before finishing)", nil } ``` ```java Java // One subagent: a small nested agent loop with the bash tool plus report_findings. // Subagents inherit the main loop's effort level. String runSubagent(Model model, String prompt) throws InterruptedException { String subagentSystem = "You are one agent in a larger parallel fan-out, assigned a single subtask. " + "Investigate it directly, using bash to check facts rather than guessing, and finish " + "by calling report_findings exactly once. Return findings, not narration."; List messages = new ArrayList<>(); messages.add(MessageParam.builder().role(MessageParam.Role.USER).content(prompt).build()); for (int turn = 0; turn < MAX_SUBAGENT_TURNS; turn++) { MessageCreateParams params = MessageCreateParams.builder() .model(model) .maxTokens(64000L) .system(subagentSystem) .outputConfig(OutputConfig.builder().effort(EFFORT).build()) .addTool(BASH_TOOL) .addTool(REPORT_TOOL) .messages(messages) .build(); MessageAccumulator accumulator = MessageAccumulator.create(); try (var stream = client.messages().createStreaming(params, REQUEST_OPTIONS)) { stream.stream().forEach(accumulator::accumulate); } Message response = accumulator.message(); messages.add(response.toParam()); StopReason stopReason = response.stopReason().orElse(null); if (StopReason.PAUSE_TURN.equals(stopReason)) { continue; } if (!StopReason.TOOL_USE.equals(stopReason)) { String text = response.content().stream() .flatMap(block -> block.text().stream()) .map(TextBlock::text) .collect(Collectors.joining()); if (StopReason.MAX_TOKENS.equals(stopReason)) { text += "\n\n(warning: subagent response was truncated at max_tokens)"; } return text; } List toolResults = new ArrayList<>(); String report = null; for (ContentBlock block : response.content()) { if (block.toolUse().isEmpty()) { continue; } ToolUseBlock toolUse = block.toolUse().get(); ToolOutput result; if (toolUse.name().equals("report_findings")) { report = toolUse._input().convert(JsonNode.class).toPrettyString(); result = new ToolOutput("Findings recorded.", false); } else if (toolUse.name().equals("bash")) { result = handleBashBlock(toolUse); } else { result = new ToolOutput("unknown tool: " + toolUse.name(), true); } toolResults.add(ContentBlockParam.ofToolResult(ToolResultBlockParam.builder() .toolUseId(toolUse.id()) .content(result.output()) .isError(result.isError()) .build())); } if (report != null) { return report; } messages.add(MessageParam.builder() .role(MessageParam.Role.USER) .contentOfBlockParams(toolResults) .build()); } return "(subagent hit the turn limit before finishing)"; } ``` ```php PHP /** * Consume a message stream and assemble the final assistant turn from its events: * the full content-block list plus the stop reason, equivalent to what a * non-streaming create call returns. */ function drainMessageStream(iterable $events): array { $stringValue = fn ($value) => $value instanceof BackedEnum ? $value->value : $value; $blocks = []; $jsonBuffers = []; $stopReason = null; foreach ($events as $event) { $type = $stringValue($event->type); if ($type === 'content_block_start') { $blocks[$event->index] = $event->contentBlock; $jsonBuffers[$event->index] = ''; } elseif ($type === 'content_block_delta') { $block = $blocks[$event->index]; $delta = $event->delta; $deltaType = $stringValue($delta->type); if ($deltaType === 'text_delta') { $blocks[$event->index] = $block->withText($block->text . $delta->text); } elseif ($deltaType === 'input_json_delta') { $jsonBuffers[$event->index] .= $delta->partialJSON; } elseif ($deltaType === 'thinking_delta') { $blocks[$event->index] = $block->withThinking($block->thinking . $delta->thinking); } elseif ($deltaType === 'signature_delta') { $blocks[$event->index] = $block->withSignature($delta->signature); } } elseif ($type === 'message_delta') { $stopReason = $stringValue($event->delta->stopReason); } } foreach ($jsonBuffers as $index => $buffer) { if ($buffer !== '' && $blocks[$index] instanceof ToolUseBlock) { $decoded = json_decode($buffer, true); $blocks[$index] = $blocks[$index]->withInput(is_array($decoded) ? $decoded : []); } } return [array_values($blocks), $stopReason]; } /** * One subagent: a small nested agent loop with the bash tool plus report_findings. * Subagents inherit the main loop's effort level. */ function runSubagent(Client $client, string $model, string $prompt): string { $subagentSystem = 'You are one agent in a larger parallel fan-out, assigned a single subtask. ' . 'Investigate it directly, using bash to check facts rather than guessing, and finish ' . 'by calling report_findings exactly once. Return findings, not narration.'; $messages = [['role' => 'user', 'content' => $prompt]]; for ($turn = 0; $turn < MAX_SUBAGENT_TURNS; $turn++) { $stream = $client->messages->createStream( model: $model, maxTokens: 64000, system: $subagentSystem, outputConfig: ['effort' => EFFORT], tools: [BASH_TOOL, REPORT_TOOL], messages: $messages, requestOptions: ['timeout' => REQUEST_TIMEOUT_SECONDS], ); [$content, $stopReason] = drainMessageStream($stream); $messages[] = ['role' => 'assistant', 'content' => $content]; if ($stopReason === 'pause_turn') { continue; } if ($stopReason !== 'tool_use') { $text = ''; foreach ($content as $block) { if ($block instanceof TextBlock) { $text .= $block->text; } } if ($stopReason === 'max_tokens') { $text .= "\n\n(warning: subagent response was truncated at max_tokens)"; } return $text; } $report = null; $toolResults = []; foreach ($content as $block) { if (!$block instanceof ToolUseBlock) { continue; } if ($block->name === 'report_findings') { $report = json_encode($block->input, JSON_PRETTY_PRINT); $output = 'Findings recorded.'; $isError = false; } elseif ($block->name === 'bash') { [$output, $isError] = handleBashBlock($block); } else { $output = "unknown tool: {$block->name}"; $isError = true; } $toolResults[] = [ 'type' => 'tool_result', 'tool_use_id' => $block->id, 'content' => $output, 'is_error' => $isError, ]; } if ($report !== null) { return $report; } $messages[] = ['role' => 'user', 'content' => $toolResults]; } return '(subagent hit the turn limit before finishing)'; } ``` ```ruby Ruby # One subagent: a small nested agent loop with the bash tool plus report_findings. # Subagents inherit the main loop's effort level. def run_subagent(model, prompt) subagent_system = "You are one agent in a larger parallel fan-out, assigned a single subtask. " \ "Investigate it directly, using bash to check facts rather than guessing, and finish " \ "by calling report_findings exactly once. Return findings, not narration." messages = [{role: "user", content: prompt}] MAX_SUBAGENT_TURNS.times do stream = CLIENT.messages.stream( model: model, max_tokens: 64_000, system_: subagent_system, output_config: {effort: EFFORT}, tools: [BASH_TOOL, REPORT_TOOL], messages: messages, request_options: {timeout: REQUEST_TIMEOUT_SECONDS} ) response = stream.accumulated_message messages << {role: "assistant", content: assistant_content_param(response.content)} next if response.stop_reason == :pause_turn unless response.stop_reason == :tool_use text = response.content.select { |block| block.type == :text }.map(&:text).join text += "\n\n(warning: subagent response was truncated at max_tokens)" if response.stop_reason == :max_tokens return text end report = nil tool_results = [] response.content.each do |block| next unless block.type == :tool_use input = parse_tool_input(block.input) case block.name when "report_findings" report = JSON.pretty_generate(input) output, is_error = "Findings recorded.", false when "bash" output, is_error = handle_bash_block(block) else output, is_error = "unknown tool: #{block.name}", true end tool_results << { type: "tool_result", tool_use_id: block.id, content: output, is_error: is_error } end return report unless report.nil? messages << {role: "user", content: tool_results} end "(subagent hit the turn limit before finishing)" end ``` ## Journal results so reruns resume A fan-out that spawns dozens of subagents is expensive to restart from scratch. A small content-addressed journal makes it idempotent: before dispatching a subagent, look up the SHA-256 of its prompt in a local JSON file, and return the recorded result if one exists. Interrupt the run, rerun it, and only the subtasks that never finished are recomputed. The journal deduplicates across runs, not within a single fan-out wave; delete the journal file to start fresh. ```python Python _journal_lock = threading.Lock() def _load_journal() -> dict: try: with open(JOURNAL_PATH) as file: return json.load(file) or {} except (OSError, json.JSONDecodeError): return {} def journaled(prompt: str, compute) -> str: """Return a cached result for this exact prompt, or compute and persist it. This makes the fan-out resumable: interrupt the run, rerun it, and only the subtasks that never finished are recomputed. Delete the journal file to start fresh.""" key = hashlib.sha256(prompt.encode()).hexdigest() cached = _load_journal().get(key) if cached is not None: print(f"[journal] cache hit for {key[:12]}", file=sys.stderr) return cached result = compute() try: with _journal_lock: # fan-out writes from many threads journal = _load_journal() journal[key] = result temp = f"{JOURNAL_PATH}.tmp" with open(temp, "w") as file: json.dump(journal, file) os.replace(temp, JOURNAL_PATH) # atomic on POSIX and Windows except OSError as error: # the journal is best-effort; never discard a computed result print(f"[journal] write failed: {error}", file=sys.stderr) return result ``` ```typescript TypeScript let journalWriteChain = Promise.resolve(); async function loadJournal(): Promise> { try { return JSON.parse(await readFile(JOURNAL_PATH, "utf8")) ?? {}; } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") { console.error(`[journal] discarding unreadable journal: ${error}`); } return {}; } } // Return a cached result for this exact prompt, or compute and persist it. This // makes the fan-out resumable: interrupt the run, rerun it, and only the subtasks // that never finished are recomputed. Delete the journal file to start fresh. async function journaled(prompt: string, compute: () => Promise): Promise { const key = createHash("sha256").update(prompt).digest("hex"); const cached = (await loadJournal())[key]; if (cached !== undefined) { console.error(`[journal] cache hit for ${key.slice(0, 12)}`); return cached; } const result = await compute(); // Chain writes so concurrent subagents do not clobber each other's entries. // The chain is kept settled so one failed write does not poison later ones. await (journalWriteChain = journalWriteChain .then(async () => { const journal = await loadJournal(); journal[key] = result; const temp = `${JOURNAL_PATH}.tmp`; await writeFile(temp, JSON.stringify(journal)); await rename(temp, JOURNAL_PATH); }) .catch((error) => console.error(`[journal] write failed: ${error}`))); return result; } ``` ```csharp C# SemaphoreSlim journalLock = new(1, 1); async Task> LoadJournal() { try { return JsonSerializer.Deserialize>(await File.ReadAllTextAsync(journalPath)) ?? []; } catch (Exception error) when (error is IOException or UnauthorizedAccessException or JsonException) { return []; } } // Return a cached result for this exact prompt, or compute and persist it. This // makes the fan-out resumable: interrupt the run, rerun it, and only the subtasks // that never finished are recomputed. Delete the journal file to start fresh. async Task Journaled(string prompt, Func> compute) { var key = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(prompt))).ToLowerInvariant(); if ((await LoadJournal()).TryGetValue(key, out var cached)) { Console.Error.WriteLine($"[journal] cache hit for {key[..12]}"); return cached; } var result = await compute(); await journalLock.WaitAsync(); // fan-out writes from many tasks try { var journal = await LoadJournal(); journal[key] = result; var temp = journalPath + ".tmp"; await File.WriteAllTextAsync(temp, JsonSerializer.Serialize(journal)); File.Move(temp, journalPath, overwrite: true); } catch (Exception error) when (error is IOException or UnauthorizedAccessException or NotSupportedException) { // The journal is best-effort; never discard a computed result. Console.Error.WriteLine($"[journal] write failed: {error.Message}"); } finally { journalLock.Release(); } return result; } ``` ```go Go var journalMutex sync.Mutex func loadJournal() map[string]string { data, err := os.ReadFile(journalPath) if err != nil { return map[string]string{} } var journal map[string]string if err := json.Unmarshal(data, &journal); err != nil || journal == nil { return map[string]string{} } return journal } // journaled returns a cached result for this exact prompt, or computes and persists // it. This makes the fan-out resumable: interrupt the run, rerun it, and only the // subtasks that never finished are recomputed. Delete the journal file to start fresh. func journaled(prompt string, compute func() (string, error)) (string, error) { sum := sha256.Sum256([]byte(prompt)) key := hex.EncodeToString(sum[:]) if cached, ok := loadJournal()[key]; ok { fmt.Fprintf(os.Stderr, "[journal] cache hit for %s\n", key[:12]) return cached, nil } result, err := compute() if err != nil { return "", err } journalMutex.Lock() // fan-out writes from many goroutines defer journalMutex.Unlock() journal := loadJournal() journal[key] = result data, _ := json.Marshal(journal) temp := journalPath + ".tmp" if err := os.WriteFile(temp, data, 0o644); err != nil { fmt.Fprintf(os.Stderr, "[journal] write failed: %s\n", err) } else if err := os.Rename(temp, journalPath); err != nil { fmt.Fprintf(os.Stderr, "[journal] write failed: %s\n", err) _ = os.Remove(temp) } return result, nil } ``` ```java Java static final ObjectMapper JOURNAL_MAPPER = new ObjectMapper(); static final ReentrantLock JOURNAL_LOCK = new ReentrantLock(); Map loadJournal() { try { return Objects.requireNonNullElseGet( JOURNAL_MAPPER.readValue(Files.readString(JOURNAL_PATH), new TypeReference>() {}), HashMap::new); } catch (IOException error) { return new HashMap<>(); } } // Return a cached result for this exact prompt, or compute and persist it. This // makes the fan-out resumable: interrupt the run, rerun it, and only the subtasks // that never finished are recomputed. Delete the journal file to start fresh. String journaled(String prompt, Callable compute) throws Exception { var digest = MessageDigest.getInstance("SHA-256").digest(prompt.getBytes(StandardCharsets.UTF_8)); String key = HexFormat.of().formatHex(digest); String cached = loadJournal().get(key); if (cached != null) { System.err.println("[journal] cache hit for " + key.substring(0, 12)); return cached; } String result = compute.call(); JOURNAL_LOCK.lock(); // fan-out writes from many threads try { Map journal = loadJournal(); journal.put(key, result); Path temp = JOURNAL_PATH.resolveSibling(JOURNAL_PATH.getFileName() + ".tmp"); Files.writeString(temp, JOURNAL_MAPPER.writeValueAsString(journal)); Files.move(temp, JOURNAL_PATH, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); } catch (IOException error) { // The journal is best-effort; never discard a computed result. System.err.println("[journal] write failed: " + error); } finally { JOURNAL_LOCK.unlock(); } return result; } ``` ```php PHP function loadJournal(): array { $raw = @file_get_contents(JOURNAL_PATH); if ($raw === false) { return []; } $decoded = json_decode($raw, true); return is_array($decoded) ? $decoded : []; } /** * Return a cached result for this exact prompt, or compute and persist it. This * makes the fan-out resumable: interrupt the run, rerun it, and only the subtasks * that never finished are recomputed. Delete the journal file to start fresh. */ function journaled(string $prompt, callable $compute): string { $key = hash('sha256', $prompt); $journal = loadJournal(); if (array_key_exists($key, $journal)) { fwrite(STDERR, '[journal] cache hit for ' . substr($key, 0, 12) . "\n"); return $journal[$key]; } $result = $compute(); $journal = loadJournal(); $journal[$key] = $result; $temp = JOURNAL_PATH . '.tmp'; $encoded = json_encode($journal, JSON_INVALID_UTF8_SUBSTITUTE); if ($encoded === false || @file_put_contents($temp, $encoded) === false || !@rename($temp, JOURNAL_PATH)) { fwrite(STDERR, '[journal] write failed: ' . (error_get_last()['message'] ?? json_last_error_msg()) . "\n"); @unlink($temp); } return $result; } ``` ```ruby Ruby JOURNAL_LOCK = Mutex.new def load_journal JSON.parse(File.read(JOURNAL_PATH)) || {} rescue SystemCallError, JSON::ParserError {} end # Return a cached result for this exact prompt, or compute and persist it. This # makes the fan-out resumable: interrupt the run, rerun it, and only the subtasks # that never finished are recomputed. Delete the journal file to start fresh. def journaled(prompt) key = Digest::SHA256.hexdigest(prompt) cached = load_journal[key] unless cached.nil? warn "[journal] cache hit for #{key[0, 12]}" return cached end result = yield begin JOURNAL_LOCK.synchronize do # fan-out writes from many threads journal = load_journal journal[key] = result temp = "#{JOURNAL_PATH}.tmp" File.write(temp, JSON.generate(journal)) File.rename(temp, JOURNAL_PATH) end rescue SystemCallError => error # the journal is best-effort; never discard a computed result warn "[journal] write failed: #{error}" end result end ``` ## Fan out, then verify The fan-out accepts up to `MAX_TOTAL_SUBTASKS` prompts, runs them through the journal with at most `MAX_CONCURRENT` in flight (sequential in the PHP port), and isolates failures so one broken subagent degrades to an error string instead of ending the run. Once the first wave finishes, a second wave reuses the same subagent path to try to refute each result: every verifier re-derives the claims from the source, defaulting to refuted when uncertain. Both the original result and its verdict are returned to the orchestrator so it can weigh them together. ```python Python def normalize_subtasks(raw) -> list[str]: """Accept the subtasks input in whatever shape the model emits: an array, the array JSON-encoded as a single string, or a newline-separated list.""" if isinstance(raw, str): try: raw = json.loads(raw) except json.JSONDecodeError: raw = raw.splitlines() if "\n" in raw else [raw] if not isinstance(raw, list): return [] return [task.strip() for task in raw if isinstance(task, str) and task.strip()] def verify_prompt_for(subtask: str, result: str) -> str: return ( "Adversarially verify the subagent result below: try to REFUTE it. Re-derive the " "claims yourself with bash rather than trusting the result, and look for evidence " "that contradicts them. Default to refuted if uncertain. Call report_findings with " "summary 'refuted: ' or 'confirmed: ', citing the file:line or command " "output that decided it.\n\n" f"Subtask: {subtask}\n\nResult to verify:\n{result}" ) def run_workflow(model: str, raw_subtasks) -> tuple[str, bool]: """Run subtasks as parallel subagents, then run a second verification wave over the results, and return both. MAX_TOTAL_SUBTASKS bounds how many the model can queue; MAX_CONCURRENT bounds how many run at once.""" all_subtasks = normalize_subtasks(raw_subtasks) subtasks = all_subtasks[:MAX_TOTAL_SUBTASKS] dropped = len(all_subtasks) - len(subtasks) if not subtasks: return "Workflow error: no usable subtasks were provided.", True print(f"[workflow] fanning out {len(subtasks)} agents", file=sys.stderr) def run_one(prompt: str) -> str: try: return journaled(prompt, lambda: run_subagent(model, prompt)) except Exception as error: # isolation boundary: one bad subagent should not end the run return f"(subagent failed: {type(error).__name__}: {error})" with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_CONCURRENT) as pool: results = list(pool.map(run_one, subtasks)) print(f"[workflow] verifying {len(results)} results", file=sys.stderr) verify_prompts = [verify_prompt_for(task, result) for task, result in zip(subtasks, results)] verdicts = list(pool.map(run_one, verify_prompts)) joined = "\n\n".join( f"[agent {index + 1}: {task}]\n{result}\n\n[verify {index + 1}]\n{verdict}" for index, (task, result, verdict) in enumerate(zip(subtasks, results, verdicts)) ) if dropped > 0: joined = ( f"(note: {dropped} subtasks beyond MAX_TOTAL_SUBTASKS={MAX_TOTAL_SUBTASKS} were not " "run; rerun them in a follow-up Workflow call)\n\n" + joined ) return joined, False ``` ```typescript TypeScript // Accept the subtasks input in whatever shape the model emits: an array, the array // JSON-encoded as a single string, or a newline-separated list. function normalizeSubtasks(raw: unknown): string[] { let value = raw; if (typeof raw === "string") { try { value = JSON.parse(raw); } catch { value = raw.includes("\n") ? raw.split("\n") : [raw]; } } if (!Array.isArray(value)) { return []; } return value .filter((task): task is string => typeof task === "string") .map((task) => task.trim()) .filter((task) => task.length > 0); } function verifyPromptFor(subtask: string, result: string): string { return ( "Adversarially verify the subagent result below: try to REFUTE it. Re-derive the " + "claims yourself with bash rather than trusting the result, and look for evidence " + "that contradicts them. Default to refuted if uncertain. Call report_findings with " + "summary 'refuted: ' or 'confirmed: ', citing the file:line or command " + "output that decided it.\n\n" + `Subtask: ${subtask}\n\nResult to verify:\n${result}` ); } // Map with a concurrency limit: at most `limit` tasks are in flight at once. async function mapWithLimit( items: readonly In[], limit: number, task: (item: In) => Promise, ): Promise { const results = new Array(items.length); let cursor = 0; const workers = Array.from({ length: Math.min(limit, items.length) }, async () => { while (cursor < items.length) { const index = cursor++; results[index] = await task(items[index]); } }); await Promise.all(workers); return results; } // Run subtasks as parallel subagents, then run a second verification wave over // the results, and return both. MAX_TOTAL_SUBTASKS bounds how many the model can // queue; MAX_CONCURRENT bounds how many run at once. async function runWorkflow( model: string, rawSubtasks: unknown, ): Promise<{ output: string; isError: boolean }> { const allSubtasks = normalizeSubtasks(rawSubtasks); const subtasks = allSubtasks.slice(0, MAX_TOTAL_SUBTASKS); const dropped = allSubtasks.length - subtasks.length; if (subtasks.length === 0) { return { output: "Workflow error: no usable subtasks were provided.", isError: true }; } console.error(`[workflow] fanning out ${subtasks.length} agents`); const runOne = async (prompt: string): Promise => { try { return await journaled(prompt, () => runSubagent(model, prompt)); } catch (error) { // Isolation boundary: one bad subagent should not end the run. const reason = error instanceof Error ? `${error.name}: ${error.message}` : String(error); return `(subagent failed: ${reason})`; } }; const results = await mapWithLimit(subtasks, MAX_CONCURRENT, runOne); console.error(`[workflow] verifying ${results.length} results`); const verifyPrompts = subtasks.map((task, index) => verifyPromptFor(task, results[index])); const verdicts = await mapWithLimit(verifyPrompts, MAX_CONCURRENT, runOne); let joined = subtasks .map( (task, index) => `[agent ${index + 1}: ${task}]\n${results[index]}\n\n[verify ${index + 1}]\n${verdicts[index]}`, ) .join("\n\n"); if (dropped > 0) { joined = `(note: ${dropped} subtasks beyond MAX_TOTAL_SUBTASKS=${MAX_TOTAL_SUBTASKS} were not ` + "run; rerun them in a follow-up Workflow call)\n\n" + joined; } return { output: joined, isError: false }; } ``` ```csharp C# // Accept the subtasks input in whatever shape the model emits: an array, the array // JSON-encoded as a single string, or a newline-separated list. List NormalizeSubtasks(JsonElement raw) { List tasks = []; if (raw.ValueKind == JsonValueKind.Array) { tasks = raw.EnumerateArray() .Where(item => item.ValueKind == JsonValueKind.String) .Select(item => item.GetString()!) .ToList(); } else if (raw.ValueKind == JsonValueKind.String) { var single = raw.GetString()!; try { tasks = JsonSerializer.Deserialize>(single) ?? []; } catch (JsonException) { tasks = [.. single.Split('\n')]; } } return tasks.Where(task => task != null).Select(task => task.Trim()).Where(task => task.Length > 0).ToList(); } string VerifyPromptFor(string subtask, string result) => "Adversarially verify the subagent result below: try to REFUTE it. Re-derive the " + "claims yourself with bash rather than trusting the result, and look for evidence " + "that contradicts them. Default to refuted if uncertain. Call report_findings with " + "summary 'refuted: ' or 'confirmed: ', citing the file:line or command " + "output that decided it.\n\n" + $"Subtask: {subtask}\n\nResult to verify:\n{result}"; // Run subtasks as parallel subagents, then run a second verification wave over // the results, and return both. maxTotalSubtasks bounds how many the model can // queue; maxConcurrent bounds how many run at once. async Task<(string Output, bool IsError)> RunWorkflow(JsonElement rawSubtasks) { var allSubtasks = NormalizeSubtasks(rawSubtasks); var subtasks = allSubtasks.Take(maxTotalSubtasks).ToList(); var dropped = allSubtasks.Count - subtasks.Count; if (subtasks.Count == 0) { return ("Workflow error: no usable subtasks were provided.", true); } Console.Error.WriteLine($"[workflow] fanning out {subtasks.Count} agents"); using SemaphoreSlim gate = new(maxConcurrent); async Task RunOne(string prompt) { await gate.WaitAsync(); try { return await Journaled(prompt, () => RunSubagent(prompt)); } catch (Exception error) { // Isolation boundary: one bad subagent should not end the run. return $"(subagent failed: {error.GetType().Name}: {error.Message})"; } finally { gate.Release(); } } var results = await Task.WhenAll(subtasks.Select(RunOne)); Console.Error.WriteLine($"[workflow] verifying {results.Length} results"); var verifyPrompts = subtasks.Select((task, index) => VerifyPromptFor(task, results[index])).ToList(); var verdicts = await Task.WhenAll(verifyPrompts.Select(RunOne)); var joined = string.Join( "\n\n", subtasks.Select((task, index) => $"[agent {index + 1}: {task}]\n{results[index]}\n\n[verify {index + 1}]\n{verdicts[index]}")); if (dropped > 0) { joined = $"(note: {dropped} subtasks beyond maxTotalSubtasks={maxTotalSubtasks} were not run; " + "rerun them in a follow-up Workflow call)\n\n" + joined; } return (joined, false); } ``` ```go Go // normalizeSubtasks accepts the subtasks input in whatever shape the model emits: an // array, the array JSON-encoded as a single string, or a newline-separated list. func normalizeSubtasks(raw json.RawMessage) []string { var tasks []string if err := json.Unmarshal(raw, &tasks); err != nil { var single string if err := json.Unmarshal(raw, &single); err != nil { return nil } if err := json.Unmarshal([]byte(single), &tasks); err != nil { tasks = strings.Split(single, "\n") } } cleaned := make([]string, 0, len(tasks)) for _, task := range tasks { if trimmed := strings.TrimSpace(task); trimmed != "" { cleaned = append(cleaned, trimmed) } } return cleaned } func verifyPromptFor(subtask, result string) string { return "Adversarially verify the subagent result below: try to REFUTE it. Re-derive the " + "claims yourself with bash rather than trusting the result, and look for evidence " + "that contradicts them. Default to refuted if uncertain. Call report_findings with " + "summary 'refuted: ' or 'confirmed: ', citing the file:line or command " + "output that decided it.\n\n" + "Subtask: " + subtask + "\n\nResult to verify:\n" + result } // mapWithLimit runs task over items with at most limit goroutines in flight. func mapWithLimit(items []string, limit int, task func(string) string) []string { results := make([]string, len(items)) semaphore := make(chan struct{}, limit) var waitGroup sync.WaitGroup for index, item := range items { waitGroup.Add(1) semaphore <- struct{}{} go func() { defer waitGroup.Done() defer func() { <-semaphore }() results[index] = task(item) }() } waitGroup.Wait() return results } // runWorkflow runs subtasks as parallel subagents, then runs a second verification wave // over the results, and returns both. maxTotalSubtasks bounds how many the model can // queue; maxConcurrent bounds how many run at once. func runWorkflow(ctx context.Context, model string, rawSubtasks json.RawMessage) (string, bool) { allSubtasks := normalizeSubtasks(rawSubtasks) subtasks := allSubtasks if len(subtasks) > maxTotalSubtasks { subtasks = subtasks[:maxTotalSubtasks] } dropped := len(allSubtasks) - len(subtasks) if len(subtasks) == 0 { return "Workflow error: no usable subtasks were provided.", true } fmt.Fprintf(os.Stderr, "[workflow] fanning out %d agents\n", len(subtasks)) runOne := func(prompt string) string { report, err := journaled(prompt, func() (string, error) { return runSubagent(ctx, model, prompt) }) if err != nil { // Isolation boundary: one bad subagent should not end the run. return fmt.Sprintf("(subagent failed: %s)", err) } return report } results := mapWithLimit(subtasks, maxConcurrent, runOne) fmt.Fprintf(os.Stderr, "[workflow] verifying %d results\n", len(results)) verifyPrompts := make([]string, len(subtasks)) for index, task := range subtasks { verifyPrompts[index] = verifyPromptFor(task, results[index]) } verdicts := mapWithLimit(verifyPrompts, maxConcurrent, runOne) sections := make([]string, len(subtasks)) for index, task := range subtasks { sections[index] = fmt.Sprintf("[agent %d: %s]\n%s\n\n[verify %d]\n%s", index+1, task, results[index], index+1, verdicts[index]) } joined := strings.Join(sections, "\n\n") if dropped > 0 { joined = fmt.Sprintf("(note: %d subtasks beyond maxTotalSubtasks=%d were not run; "+ "rerun them in a follow-up Workflow call)\n\n", dropped, maxTotalSubtasks) + joined } return joined, false } ``` ```java Java // Accept the subtasks input in whatever shape the model emits: an array, the array // JSON-encoded as a single string, or a newline-separated list. List normalizeSubtasks(JsonValue raw) { List tasks = new ArrayList<>(); if (raw.asArray().isPresent()) { for (JsonValue item : (List) raw.asArray().get()) { tasks.add(item.asString().isPresent() ? item.asStringOrThrow() : item.toString()); } } else if (raw.asString().isPresent()) { String single = raw.asStringOrThrow(); try { String[] parsed = new ObjectMapper().readValue(single, String[].class); if (parsed != null) { for (String task : parsed) { tasks.add(task); } } } catch (JsonProcessingException error) { for (String task : single.split("\n")) { tasks.add(task); } } } return tasks.stream() .filter(task -> task != null) .map(String::trim) .filter(task -> !task.isEmpty()) .toList(); } String verifyPromptFor(String subtask, String result) { return "Adversarially verify the subagent result below: try to REFUTE it. Re-derive the " + "claims yourself with bash rather than trusting the result, and look for evidence " + "that contradicts them. Default to refuted if uncertain. Call report_findings with " + "summary 'refuted: ' or 'confirmed: ', citing the file:line or command " + "output that decided it.\n\n" + "Subtask: " + subtask + "\n\nResult to verify:\n" + result; } List runAll(ExecutorService pool, List prompts, Model model) throws InterruptedException { List> jobs = prompts.stream() .>map(prompt -> () -> journaled(prompt, () -> runSubagent(model, prompt))) .toList(); List results = new ArrayList<>(); for (Future future : pool.invokeAll(jobs)) { try { results.add(future.get()); } catch (ExecutionException | CancellationException error) { // Isolation boundary: one bad subagent should not end the run. Throwable cause = error.getCause() != null ? error.getCause() : error; results.add("(subagent failed: " + cause + ")"); } } return results; } // Run subtasks as parallel subagents, then run a second verification wave over // the results, and return both. MAX_TOTAL_SUBTASKS bounds how many the model can // queue; MAX_CONCURRENT bounds how many run at once. ToolOutput runWorkflow(Model model, JsonValue rawSubtasks) throws InterruptedException { List allSubtasks = normalizeSubtasks(rawSubtasks); List subtasks = allSubtasks.stream().limit(MAX_TOTAL_SUBTASKS).toList(); int dropped = allSubtasks.size() - subtasks.size(); if (subtasks.isEmpty()) { return new ToolOutput("Workflow error: no usable subtasks were provided.", true); } System.err.println("[workflow] fanning out " + subtasks.size() + " agents"); List results; List verdicts; try (ExecutorService pool = Executors.newFixedThreadPool(MAX_CONCURRENT, Thread.ofVirtual().factory())) { results = runAll(pool, subtasks, model); System.err.println("[workflow] verifying " + results.size() + " results"); List verifyPrompts = IntStream.range(0, subtasks.size()) .mapToObj(index -> verifyPromptFor(subtasks.get(index), results.get(index))) .toList(); verdicts = runAll(pool, verifyPrompts, model); } String joined = IntStream.range(0, subtasks.size()) .mapToObj(index -> "[agent " + (index + 1) + ": " + subtasks.get(index) + "]\n" + results.get(index) + "\n\n[verify " + (index + 1) + "]\n" + verdicts.get(index)) .collect(Collectors.joining("\n\n")); if (dropped > 0) { joined = "(note: " + dropped + " subtasks beyond MAX_TOTAL_SUBTASKS=" + MAX_TOTAL_SUBTASKS + " were not run; rerun them in a follow-up Workflow call)\n\n" + joined; } return new ToolOutput(joined, false); } ``` ```php PHP /** * Accept the subtasks input in whatever shape the model emits: an array, the array * JSON-encoded as a single string, or a newline-separated list. */ function normalizeSubtasks(mixed $raw): array { if (is_string($raw)) { try { $raw = json_decode($raw, true, flags: JSON_THROW_ON_ERROR); } catch (JsonException) { $raw = str_contains($raw, "\n") ? explode("\n", $raw) : [$raw]; } } if (!is_array($raw)) { return []; } $tasks = array_map('trim', array_filter($raw, 'is_string')); return array_values(array_filter($tasks, fn ($task) => $task !== '')); } function verifyPromptFor(string $subtask, string $result): string { return 'Adversarially verify the subagent result below: try to REFUTE it. Re-derive the ' . 'claims yourself with bash rather than trusting the result, and look for evidence ' . 'that contradicts them. Default to refuted if uncertain. Call report_findings with ' . "summary 'refuted: ' or 'confirmed: ', citing the file:line or command " . "output that decided it.\n\n" . "Subtask: {$subtask}\n\nResult to verify:\n{$result}"; } /** * Run subtasks through the journal, then run a second verification wave over the * results, and return both. PHP's standard runtime has no lightweight thread pool, * so both waves run sequentially here (MAX_CONCURRENT is unused); the SDK examples * in other languages fan them out in parallel. */ function runWorkflow(Client $client, string $model, mixed $rawSubtasks): array { $allSubtasks = normalizeSubtasks($rawSubtasks); $subtasks = array_slice($allSubtasks, 0, MAX_TOTAL_SUBTASKS); $dropped = count($allSubtasks) - count($subtasks); if ($subtasks === []) { return ['Workflow error: no usable subtasks were provided.', true]; } fwrite(STDERR, '[workflow] running ' . count($subtasks) . " agents\n"); $runOne = function (string $prompt) use ($client, $model): string { try { return journaled($prompt, fn () => runSubagent($client, $model, $prompt)); } catch (Throwable $error) { // Isolation boundary: one bad subagent should not end the run. return '(subagent failed: ' . $error::class . ': ' . $error->getMessage() . ')'; } }; $results = array_map($runOne, $subtasks); fwrite(STDERR, '[workflow] verifying ' . count($results) . " results\n"); $verifyPrompts = array_map(verifyPromptFor(...), $subtasks, $results); $verdicts = array_map($runOne, $verifyPrompts); $sections = []; foreach ($subtasks as $index => $task) { $sections[] = '[agent ' . ($index + 1) . ": {$task}]\n{$results[$index]}" . "\n\n[verify " . ($index + 1) . "]\n{$verdicts[$index]}"; } $joined = implode("\n\n", $sections); if ($dropped > 0) { $joined = '(note: ' . $dropped . ' subtasks beyond MAX_TOTAL_SUBTASKS=' . MAX_TOTAL_SUBTASKS . " were not run; rerun them in a follow-up Workflow call)\n\n" . $joined; } return [$joined, false]; } ``` ```ruby Ruby # Accept the subtasks input in whatever shape the model emits: an array, the array # JSON-encoded as a single string, or a newline-separated list. def normalize_subtasks(raw) if raw.is_a?(String) begin raw = JSON.parse(raw) rescue JSON::ParserError raw = raw.include?("\n") ? raw.split("\n") : [raw] end end return [] unless raw.is_a?(Array) raw.select { |task| task.is_a?(String) }.map(&:strip).reject(&:empty?) end def verify_prompt_for(subtask, result) "Adversarially verify the subagent result below: try to REFUTE it. Re-derive the " \ "claims yourself with bash rather than trusting the result, and look for evidence " \ "that contradicts them. Default to refuted if uncertain. Call report_findings with " \ "summary 'refuted: ' or 'confirmed: ', citing the file:line or command " \ "output that decided it.\n\n" \ "Subtask: #{subtask}\n\nResult to verify:\n#{result}" end # Map with a concurrency limit: at most `limit` threads are in flight at once. def map_with_limit(items, limit) results = Array.new(items.length) queue = Queue.new items.each_with_index { |item, index| queue << [index, item] } workers = Array.new([limit, items.length].min) do Thread.new do until queue.empty? index, item = queue.pop(true) rescue break results[index] = yield item end end end workers.each(&:join) results end # Run subtasks as parallel subagents, then run a second verification wave over # the results, and return both. MAX_TOTAL_SUBTASKS bounds how many the model can # queue; MAX_CONCURRENT bounds how many run at once. def run_workflow(model, raw_subtasks) all_subtasks = normalize_subtasks(raw_subtasks) subtasks = all_subtasks.first(MAX_TOTAL_SUBTASKS) dropped = all_subtasks.length - subtasks.length return ["Workflow error: no usable subtasks were provided.", true] if subtasks.empty? warn "[workflow] fanning out #{subtasks.length} agents" run_one = lambda do |prompt| journaled(prompt) { run_subagent(model, prompt) } rescue => error # isolation boundary: one bad subagent should not end the run "(subagent failed: #{error.class}: #{error.message})" end results = map_with_limit(subtasks, MAX_CONCURRENT, &run_one) warn "[workflow] verifying #{results.length} results" verify_prompts = subtasks.zip(results).map { |task, result| verify_prompt_for(task, result) } verdicts = map_with_limit(verify_prompts, MAX_CONCURRENT, &run_one) joined = subtasks.each_with_index.map do |task, index| "[agent #{index + 1}: #{task}]\n#{results[index]}\n\n[verify #{index + 1}]\n#{verdicts[index]}" end.join("\n\n") if dropped > 0 joined = "(note: #{dropped} subtasks beyond MAX_TOTAL_SUBTASKS=#{MAX_TOTAL_SUBTASKS} were not " \ "run; rerun them in a follow-up Workflow call)\n\n#{joined}" end [joined, false] end ``` ## Toggle the mode with mid-conversation system messages The agent appends the user's message first, then any system messages that are due: the exit notice, the full mode text on entry, or the periodic refresher. Placing the system message after the user turn keeps every cached byte ahead of it untouched, and satisfies the placement rule that a system message follows a user turn. ```bash cURL # One orchestration-mode turn: the mode reminder rides in the messages array as a # {"role": "system"} entry placed after the user turn it applies to. The response # stops at the first tool call. The agent loop that executes tool calls and fans # out subagents is shown in the SDK tabs; the Workflow description is condensed # here, the SDK examples carry the full standing-consent text. curl --fail-with-body -sS https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d @- <<'EOF' { "model": "claude-opus-5", "max_tokens": 64000, "system": "You are a helpful general-purpose agent. Answer the user's request directly.", "output_config": {"effort": "xhigh"}, "tools": [ { "name": "Workflow", "description": "Orchestrate a multiagent workflow: split a large task into independent subtasks and run them as parallel agents, then collect their results. Opt-in: only use this tool when the user explicitly asks for a workflow, or when a system message confirms that orchestration mode is on. Granularity: scope each subtask to a distinct concern rather than per line or file section; a review of a small module rarely needs more than about ten. Standing consent: while a system message confirms orchestration mode is on, author and run a workflow for every substantive task by default; work solo only on conversational turns or trivial mechanical edits.", "input_schema": { "type": "object", "properties": { "subtasks": { "type": "array", "items": {"type": "string"}, "description": "Independent subtask prompts to run as parallel agents" } }, "required": ["subtasks"] } }, {"type": "bash_20250124", "name": "bash"} ], "messages": [ { "role": "user", "content": "Explore the current directory, then give a thorough review: what it does, code-quality issues, and concrete improvements." }, { "role": "system", "content": "Orchestration mode is on: optimize for the most exhaustive, correct answer rather than the fastest one. Use the Workflow tool on every substantive task, sized to the problem's natural decomposition rather than the maximum the tool allows. See the Workflow tool's description for standing consent, granularity guidance, and quality patterns. Work solo only on conversational or trivial turns." } ] } EOF ``` ```bash CLI # One orchestration-mode turn: the mode reminder rides in the messages array as a # system-role entry placed after the user turn it applies to. The response stops # at the first tool call. The agent loop that executes tool calls and fans out # subagents is shown in the SDK tabs; the Workflow description is condensed here, # the SDK examples carry the full standing-consent text. ant messages create <<'YAML' model: claude-opus-5 max_tokens: 64000 system: You are a helpful general-purpose agent. Answer the user's request directly. output_config: {effort: xhigh} tools: - name: Workflow description: >- Orchestrate a multiagent workflow: split a large task into independent subtasks and run them as parallel agents, then collect their results. Opt-in: only use this tool when the user explicitly asks for a workflow, or when a system message confirms that orchestration mode is on. Granularity: scope each subtask to a distinct concern rather than per line or file section; a review of a small module rarely needs more than about ten. Standing consent: while a system message confirms orchestration mode is on, author and run a workflow for every substantive task by default; work solo only on conversational turns or trivial mechanical edits. input_schema: type: object properties: subtasks: type: array items: {type: string} description: Independent subtask prompts to run as parallel agents required: [subtasks] - {type: bash_20250124, name: bash} messages: - role: user content: >- Explore the current directory, then give a thorough review: what it does, code-quality issues, and concrete improvements. - role: system content: >- Orchestration mode is on: optimize for the most exhaustive, correct answer rather than the fastest one. Use the Workflow tool on every substantive task, sized to the problem's natural decomposition rather than the maximum the tool allows. See the Workflow tool's description for standing consent, granularity guidance, and quality patterns. Work solo only on conversational or trivial turns. YAML ``` ```python Python class ModeAgent: """An agent loop whose orchestration mode is toggled with mid-conversation system messages.""" def __init__(self, model: str, mode_on: bool = True): self.model = model self.mode_on = mode_on self.messages: list[dict] = [] self._mode_announced = False self._exit_pending = False self._turns_since_reminder = 0 def set_mode(self, mode_on: bool) -> None: """Turn the mode on or off. The notice is delivered with the next user turn.""" if mode_on == self.mode_on: return if not mode_on: if self._mode_announced: self._exit_pending = True else: self._exit_pending = False self.mode_on = mode_on def _due_system_messages(self) -> list[dict]: """System messages owed on this turn: an exit notice, the full mode text on entry, or a one-line refresher every TURNS_BETWEEN_REFRESHERS user turns.""" due = [] if self._exit_pending: self._exit_pending = False self._mode_announced = False due.append({"role": "system", "content": MODE_EXIT}) if self.mode_on: if not self._mode_announced: self._mode_announced = True self._turns_since_reminder = 0 due.append({"role": "system", "content": MODE_ENTER}) elif self._turns_since_reminder >= TURNS_BETWEEN_REFRESHERS: self._turns_since_reminder = 0 due.append({"role": "system", "content": MODE_REFRESH}) return due def turn(self, user_input: str) -> str: # Mid-conversation system messages follow the user turn they apply to, which keeps # the cached prefix ahead of them untouched. self.messages.append({"role": "user", "content": user_input}) self.messages.extend(self._due_system_messages()) self._turns_since_reminder += 1 for _ in range(MAX_MAIN_TURNS): with client.messages.stream( model=self.model, max_tokens=64000, system=SYSTEM_PROMPT, # static for the whole session output_config={"effort": EFFORT}, tools=[WORKFLOW_TOOL, BASH_TOOL], messages=self.messages, timeout=REQUEST_TIMEOUT_SECONDS, ) as stream: response = stream.get_final_message() self.messages.append({"role": "assistant", "content": response.content}) if response.stop_reason == "pause_turn": continue if response.stop_reason != "tool_use": text = "".join(block.text for block in response.content if block.type == "text") if response.stop_reason == "max_tokens": # Drop the truncated assistant message so later turns don't build on it. self.messages.pop() text += "\n\n(warning: response was truncated at max_tokens)" return text tool_results = [] for block in response.content: if block.type != "tool_use": continue if block.name == "Workflow": output, is_error = run_workflow(self.model, block.input.get("subtasks", [])) elif block.name == "bash": output, is_error = handle_bash_block(block) else: output, is_error = f"unknown tool: {block.name}", True tool_results.append( { "type": "tool_result", "tool_use_id": block.id, "content": output, "is_error": is_error, } ) self.messages.append({"role": "user", "content": tool_results}) return "(hit the main loop turn limit before finishing)" ``` ```typescript TypeScript // An agent loop whose orchestration mode is toggled with mid-conversation system messages. class ModeAgent { private readonly model: string; private modeOn: boolean; private readonly messages: Anthropic.MessageParam[] = []; private modeAnnounced = false; private exitPending = false; private turnsSinceReminder = 0; constructor(model: string, modeOn = true) { this.model = model; this.modeOn = modeOn; } // Turn the mode on or off. The notice is delivered with the next user turn. setMode(modeOn: boolean): void { if (modeOn === this.modeOn) { return; } if (!modeOn) { if (this.modeAnnounced) { this.exitPending = true; } } else { this.exitPending = false; } this.modeOn = modeOn; } // System messages owed on this turn: an exit notice, the full mode text on entry, // or a one-line refresher every TURNS_BETWEEN_REFRESHERS user turns. private dueSystemMessages(): Anthropic.MessageParam[] { const due: Array<{ role: "system"; content: string }> = []; if (this.exitPending) { this.exitPending = false; this.modeAnnounced = false; due.push({ role: "system", content: MODE_EXIT }); } if (this.modeOn) { if (!this.modeAnnounced) { this.modeAnnounced = true; this.turnsSinceReminder = 0; due.push({ role: "system", content: MODE_ENTER }); } else if (this.turnsSinceReminder >= TURNS_BETWEEN_REFRESHERS) { this.turnsSinceReminder = 0; due.push({ role: "system", content: MODE_REFRESH }); } } // The published SDK types message roles as "user" | "assistant"; typed support for // mid-conversation system messages ships with the SDK release that includes them. return due as unknown as Anthropic.MessageParam[]; } async turn(userInput: string): Promise { // Mid-conversation system messages follow the user turn they apply to, which keeps // the cached prefix ahead of them untouched. this.messages.push({ role: "user", content: userInput }); this.messages.push(...this.dueSystemMessages()); this.turnsSinceReminder += 1; for (let turn = 0; turn < MAX_MAIN_TURNS; turn++) { const response = await client.messages .stream( { model: this.model, max_tokens: 64000, system: SYSTEM_PROMPT, // static for the whole session output_config: { effort: EFFORT }, tools: [WORKFLOW_TOOL, BASH_TOOL], messages: this.messages, }, { signal: AbortSignal.timeout(REQUEST_TIMEOUT_SECONDS * 1000) }, ) .finalMessage(); this.messages.push({ role: "assistant", content: response.content }); if (response.stop_reason === "pause_turn") { continue; } if (response.stop_reason !== "tool_use") { let text = response.content .filter((block): block is Anthropic.TextBlock => block.type === "text") .map((block) => block.text) .join(""); if (response.stop_reason === "max_tokens") { // Drop the truncated assistant message so later turns do not build on it. this.messages.pop(); text += "\n\n(warning: response was truncated at max_tokens)"; } return text; } const toolResults: Anthropic.ToolResultBlockParam[] = []; for (const block of response.content) { if (block.type !== "tool_use") { continue; } let output: string; let isError: boolean; if (block.name === "Workflow") { const input = block.input as { subtasks?: unknown }; ({ output, isError } = await runWorkflow(this.model, input.subtasks ?? [])); } else if (block.name === "bash") { ({ output, isError } = await handleBashBlock(block)); } else { output = `unknown tool: ${block.name}`; isError = true; } toolResults.push({ type: "tool_result", tool_use_id: block.id, content: output, is_error: isError, }); } this.messages.push({ role: "user", content: toolResults }); } return "(hit the main loop turn limit before finishing)"; } } ``` ```csharp C# // An agent loop whose orchestration mode is toggled with mid-conversation system messages. List messages = []; var modeOn = true; var modeAnnounced = false; var exitPending = false; var turnsSinceReminder = 0; // Turn the mode on or off. The notice is delivered with the next user turn. void SetMode(bool nextModeOn) { if (nextModeOn == modeOn) { return; } if (!nextModeOn) { if (modeAnnounced) { exitPending = true; } } else { exitPending = false; } modeOn = nextModeOn; } // The Role property is an open enum, so the mid-conversation "system" role can be assigned // as a raw string; a dedicated constant ships with the SDK release. MessageParam SystemMessage(string content) => new() { Role = "system", Content = content }; // System messages owed on this turn: an exit notice, the full mode text on entry, // or a one-line refresher every turnsBetweenRefreshers user turns. List DueSystemMessages() { List due = []; if (exitPending) { exitPending = false; modeAnnounced = false; due.Add(SystemMessage(modeExit)); } if (modeOn) { if (!modeAnnounced) { modeAnnounced = true; turnsSinceReminder = 0; due.Add(SystemMessage(modeEnter)); } else if (turnsSinceReminder >= turnsBetweenRefreshers) { turnsSinceReminder = 0; due.Add(SystemMessage(modeRefresh)); } } return due; } // Send one user turn through the loop, executing tool calls until the model stops. async Task Turn(string userInput) { // Mid-conversation system messages follow the user turn they apply to, which keeps // the cached prefix ahead of them untouched. messages.Add(new() { Role = Role.User, Content = userInput }); messages.AddRange(DueSystemMessages()); turnsSinceReminder++; for (var turn = 0; turn < maxMainTurns; turn++) { using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(requestTimeoutSeconds)); var response = await client.Messages.Create(new MessageCreateParams { Model = model, MaxTokens = requestMaxTokens, System = systemPrompt, // static for the whole session OutputConfig = new OutputConfig { Effort = effort }, Tools = [workflowTool, bashTool], Messages = messages, }, cancellationToken: deadline.Token); messages.Add(new() { Role = Role.Assistant, Content = response.Content.Select(block => new ContentBlockParam(block.Json)).ToList(), }); if (response.StopReason == StopReason.PauseTurn) { continue; } if (response.StopReason != StopReason.ToolUse) { var text = string.Concat( response.Content.Select(block => block.TryPickText(out var textBlock) ? textBlock.Text : "")); if (response.StopReason == StopReason.MaxTokens) { // Drop the truncated assistant message so the next turn does not build on it. messages.RemoveAt(messages.Count - 1); text += "\n\n(warning: response was truncated at max_tokens)"; } return text; } List toolResults = []; foreach (var block in response.Content) { if (!block.TryPickToolUse(out var toolUse)) { continue; } string output; bool isError; if (toolUse.Name == "Workflow") { toolUse.Input.TryGetValue("subtasks", out var rawSubtasks); (output, isError) = await RunWorkflow(rawSubtasks); } else if (toolUse.Name == "bash") { (output, isError) = await HandleBashBlock(toolUse); } else { output = $"unknown tool: {toolUse.Name}"; isError = true; } toolResults.Add(new ToolResultBlockParam(toolUse.ID) { Content = output, IsError = isError }); } messages.Add(new() { Role = Role.User, Content = toolResults }); } return "(hit the main loop turn limit before finishing)"; } ``` ```go Go // modeAgent is an agent loop whose orchestration mode is toggled with mid-conversation // system messages. type modeAgent struct { model string modeOn bool messages []anthropic.MessageParam modeAnnounced bool exitPending bool turnsSinceReminder int } func newModeAgent(model string) *modeAgent { return &modeAgent{model: model, modeOn: true} } // setMode turns the mode on or off. The notice is delivered with the next user turn. func (agent *modeAgent) setMode(modeOn bool) { if modeOn == agent.modeOn { return } if !modeOn { if agent.modeAnnounced { agent.exitPending = true } } else { agent.exitPending = false } agent.modeOn = modeOn } // dueSystemMessages returns the system messages owed on this turn: an exit notice, the // full mode text on entry, or a one-line refresher every turnsBetweenRefreshers user turns. func (agent *modeAgent) dueSystemMessages() []anthropic.MessageParam { // MessageParamRole is an open string type, so the mid-conversation "system" role can // be expressed directly; a dedicated constant ships with the SDK release. systemMessage := func(content string) anthropic.MessageParam { return anthropic.MessageParam{ Role: anthropic.MessageParamRole("system"), Content: []anthropic.ContentBlockParamUnion{anthropic.NewTextBlock(content)}, } } var due []anthropic.MessageParam if agent.exitPending { agent.exitPending = false agent.modeAnnounced = false due = append(due, systemMessage(modeExit)) } if agent.modeOn { if !agent.modeAnnounced { agent.modeAnnounced = true agent.turnsSinceReminder = 0 due = append(due, systemMessage(modeEnter)) } else if agent.turnsSinceReminder >= turnsBetweenRefreshers { agent.turnsSinceReminder = 0 due = append(due, systemMessage(modeRefresh)) } } return due } // turn sends one user turn through the loop, executing tool calls until the model stops. func (agent *modeAgent) turn(ctx context.Context, userInput string) (string, error) { // Mid-conversation system messages follow the user turn they apply to, which keeps // the cached prefix ahead of them untouched. agent.messages = append(agent.messages, anthropic.NewUserMessage(anthropic.NewTextBlock(userInput))) agent.messages = append(agent.messages, agent.dueSystemMessages()...) agent.turnsSinceReminder++ for range maxMainTurns { var response anthropic.Message err := func() error { ctx, cancel := context.WithTimeout(ctx, requestTimeoutSeconds*time.Second) defer cancel() stream := client.Messages.NewStreaming(ctx, anthropic.MessageNewParams{ Model: agent.model, MaxTokens: 64000, System: []anthropic.TextBlockParam{{Text: systemPrompt}}, // static for the whole session OutputConfig: anthropic.OutputConfigParam{Effort: effort}, Tools: []anthropic.ToolUnionParam{workflowTool, bashTool}, Messages: agent.messages, }) defer stream.Close() for stream.Next() { if err := response.Accumulate(stream.Current()); err != nil { return err } } return stream.Err() }() if err != nil { return "", err } agent.messages = append(agent.messages, response.ToParam()) if response.StopReason == anthropic.StopReasonPauseTurn { continue } if response.StopReason != anthropic.StopReasonToolUse { var text strings.Builder for _, block := range response.Content { if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok { text.WriteString(textBlock.Text) } } if response.StopReason == anthropic.StopReasonMaxTokens { // Drop the truncated assistant message rather than leave a clipped turn in history. agent.messages = agent.messages[:len(agent.messages)-1] text.WriteString("\n\n(warning: response was truncated at max_tokens)") } return text.String(), nil } var toolResults []anthropic.ContentBlockParamUnion for _, block := range response.Content { toolUse, ok := block.AsAny().(anthropic.ToolUseBlock) if !ok { continue } var output string var isError bool switch toolUse.Name { case "Workflow": var input struct { Subtasks json.RawMessage `json:"subtasks"` } if err := json.Unmarshal(toolUse.Input, &input); err != nil { output, isError = fmt.Sprintf("Workflow error: could not parse input: %s", err), true } else { output, isError = runWorkflow(ctx, agent.model, input.Subtasks) } case "bash": output, isError = handleBashBlock(ctx, toolUse) default: output, isError = fmt.Sprintf("unknown tool: %s", toolUse.Name), true } toolResults = append(toolResults, anthropic.NewToolResultBlock(toolUse.ID, output, isError)) } agent.messages = append(agent.messages, anthropic.NewUserMessage(toolResults...)) } return "(hit the main loop turn limit before finishing)", nil } ``` ```java Java // An agent loop whose orchestration mode is toggled with mid-conversation system messages. class ModeAgent { private final Model model; private boolean modeOn; private final List messages = new ArrayList<>(); private boolean modeAnnounced = false; private boolean exitPending = false; private int turnsSinceReminder = 0; ModeAgent(Model model) { this(model, true); } ModeAgent(Model model, boolean modeOn) { this.model = model; this.modeOn = modeOn; } // Turn the mode on or off. The notice is delivered with the next user turn. void setMode(boolean modeOn) { if (modeOn == this.modeOn) { return; } if (!modeOn) { if (modeAnnounced) { exitPending = true; } } else { exitPending = false; } this.modeOn = modeOn; } // System messages owed on this turn: an exit notice, the full mode text on entry, // or a one-line refresher every TURNS_BETWEEN_REFRESHERS user turns. private List dueSystemMessages() { List due = new ArrayList<>(); if (exitPending) { exitPending = false; modeAnnounced = false; due.add(systemMessage(MODE_EXIT)); } if (modeOn) { if (!modeAnnounced) { modeAnnounced = true; turnsSinceReminder = 0; due.add(systemMessage(MODE_ENTER)); } else if (turnsSinceReminder >= TURNS_BETWEEN_REFRESHERS) { turnsSinceReminder = 0; due.add(systemMessage(MODE_REFRESH)); } } return due; } // MessageParam.Role is an open enum, so the mid-conversation "system" role can be // expressed with Role.of; a dedicated constant ships with the SDK release. private MessageParam systemMessage(String content) { return MessageParam.builder() .role(MessageParam.Role.of("system")) .content(content) .build(); } // Send one user turn through the loop, executing tool calls until the model stops. String turn(String userInput) throws InterruptedException { // Mid-conversation system messages follow the user turn they apply to, which keeps // the cached prefix ahead of them untouched. messages.add(MessageParam.builder().role(MessageParam.Role.USER).content(userInput).build()); messages.addAll(dueSystemMessages()); turnsSinceReminder++; for (int turn = 0; turn < MAX_MAIN_TURNS; turn++) { MessageCreateParams params = MessageCreateParams.builder() .model(model) .maxTokens(64000L) .system(SYSTEM_PROMPT) // static for the whole session .outputConfig(OutputConfig.builder().effort(EFFORT).build()) .addTool(WORKFLOW_TOOL) .addTool(BASH_TOOL) .messages(messages) .build(); MessageAccumulator accumulator = MessageAccumulator.create(); try (var stream = client.messages().createStreaming(params, REQUEST_OPTIONS)) { stream.stream().forEach(accumulator::accumulate); } Message response = accumulator.message(); messages.add(response.toParam()); StopReason stopReason = response.stopReason().orElse(null); if (StopReason.PAUSE_TURN.equals(stopReason)) { continue; } if (!StopReason.TOOL_USE.equals(stopReason)) { String text = response.content().stream() .flatMap(block -> block.text().stream()) .map(TextBlock::text) .collect(Collectors.joining()); if (StopReason.MAX_TOKENS.equals(stopReason)) { // Drop the truncated assistant message so it does not poison later turns. messages.removeLast(); text += "\n\n(warning: response was truncated at max_tokens)"; } return text; } List toolResults = new ArrayList<>(); for (ContentBlock block : response.content()) { if (block.toolUse().isEmpty()) { continue; } ToolUseBlock toolUse = block.toolUse().get(); ToolOutput result = switch (toolUse.name()) { case "Workflow" -> { Map input = (Map) toolUse._input().asObject().orElse(Map.of()); JsonValue rawSubtasks = input.getOrDefault("subtasks", JsonValue.from(List.of())); yield runWorkflow(model, rawSubtasks); } case "bash" -> handleBashBlock(toolUse); default -> new ToolOutput("unknown tool: " + toolUse.name(), true); }; toolResults.add(ContentBlockParam.ofToolResult(ToolResultBlockParam.builder() .toolUseId(toolUse.id()) .content(result.output()) .isError(result.isError()) .build())); } messages.add(MessageParam.builder() .role(MessageParam.Role.USER) .contentOfBlockParams(toolResults) .build()); } return "(hit the main loop turn limit before finishing)"; } } ``` ```php PHP /** An agent loop whose orchestration mode is toggled with mid-conversation system messages. */ class ModeAgent { private array $messages = []; private bool $modeAnnounced = false; private bool $exitPending = false; private int $turnsSinceReminder = 0; public function __construct( private readonly Client $client, private readonly string $model, private bool $modeOn = true, ) { } /** Turn the mode on or off. The notice is delivered with the next user turn. */ public function setMode(bool $modeOn): void { if ($modeOn === $this->modeOn) { return; } if ($modeOn) { $this->exitPending = false; } elseif ($this->modeAnnounced) { $this->exitPending = true; } $this->modeOn = $modeOn; } public function turn(string $userInput): string { // Mid-conversation system messages follow the user turn they apply to, which keeps // the cached prefix ahead of them untouched. $this->messages[] = ['role' => 'user', 'content' => $userInput]; array_push($this->messages, ...$this->dueSystemMessages()); $this->turnsSinceReminder++; for ($turn = 0; $turn < MAX_MAIN_TURNS; $turn++) { $stream = $this->client->messages->createStream( model: $this->model, maxTokens: 64000, system: SYSTEM_PROMPT, // static for the whole session outputConfig: ['effort' => EFFORT], tools: [WORKFLOW_TOOL, BASH_TOOL], messages: $this->messages, requestOptions: ['timeout' => REQUEST_TIMEOUT_SECONDS], ); [$content, $stopReason] = drainMessageStream($stream); $this->messages[] = ['role' => 'assistant', 'content' => $content]; if ($stopReason === 'pause_turn') { continue; } if ($stopReason !== 'tool_use') { $text = ''; foreach ($content as $block) { if ($block instanceof TextBlock) { $text .= $block->text; } } if ($stopReason === 'max_tokens') { // Drop the truncated assistant message so the next turn does not build on it. array_pop($this->messages); $text .= "\n\n(warning: response was truncated at max_tokens)"; } return $text; } $toolResults = []; foreach ($content as $block) { if (!$block instanceof ToolUseBlock) { continue; } if ($block->name === 'Workflow') { [$output, $isError] = runWorkflow($this->client, $this->model, $block->input['subtasks'] ?? []); } elseif ($block->name === 'bash') { [$output, $isError] = handleBashBlock($block); } else { $output = "unknown tool: {$block->name}"; $isError = true; } $toolResults[] = [ 'type' => 'tool_result', 'tool_use_id' => $block->id, 'content' => $output, 'is_error' => $isError, ]; } $this->messages[] = ['role' => 'user', 'content' => $toolResults]; } return '(hit the main loop turn limit before finishing)'; } /** * System messages owed on this turn: an exit notice, the full mode text on entry, * or a one-line refresher every TURNS_BETWEEN_REFRESHERS user turns. */ private function dueSystemMessages(): array { $due = []; if ($this->exitPending) { $this->exitPending = false; $this->modeAnnounced = false; $due[] = ['role' => 'system', 'content' => MODE_EXIT]; } if ($this->modeOn) { if (!$this->modeAnnounced) { $this->modeAnnounced = true; $this->turnsSinceReminder = 0; $due[] = ['role' => 'system', 'content' => MODE_ENTER]; } elseif ($this->turnsSinceReminder >= TURNS_BETWEEN_REFRESHERS) { $this->turnsSinceReminder = 0; $due[] = ['role' => 'system', 'content' => MODE_REFRESH]; } } return $due; } } ``` ```ruby Ruby # An agent loop whose orchestration mode is toggled with mid-conversation system messages. class ModeAgent def initialize(model, mode_on: true) @model = model @mode_on = mode_on @messages = [] @mode_announced = false @exit_pending = false @turns_since_reminder = 0 end # Turn the mode on or off. The notice is delivered with the next user turn. def set_mode(mode_on) return if mode_on == @mode_on if mode_on @exit_pending = false else @exit_pending = true if @mode_announced end @mode_on = mode_on end def turn(user_input) # Mid-conversation system messages follow the user turn they apply to, which keeps # the cached prefix ahead of them untouched. @messages << {role: "user", content: user_input} @messages.concat(due_system_messages) @turns_since_reminder += 1 MAX_MAIN_TURNS.times do stream = CLIENT.messages.stream( model: @model, max_tokens: 64_000, system_: SYSTEM_PROMPT, # static for the whole session output_config: {effort: EFFORT}, tools: [WORKFLOW_TOOL, BASH_TOOL], messages: @messages, request_options: {timeout: REQUEST_TIMEOUT_SECONDS} ) response = stream.accumulated_message @messages << {role: "assistant", content: assistant_content_param(response.content)} next if response.stop_reason == :pause_turn unless response.stop_reason == :tool_use text = response.content.select { |block| block.type == :text }.map(&:text).join if response.stop_reason == :max_tokens @messages.pop # drop the truncated assistant message from the history text += "\n\n(warning: response was truncated at max_tokens)" end return text end tool_results = [] response.content.each do |block| next unless block.type == :tool_use input = parse_tool_input(block.input) case block.name when "Workflow" output, is_error = run_workflow(@model, input["subtasks"] || []) when "bash" output, is_error = handle_bash_block(block) else output, is_error = "unknown tool: #{block.name}", true end tool_results << { type: "tool_result", tool_use_id: block.id, content: output, is_error: is_error } end @messages << {role: "user", content: tool_results} end "(hit the main loop turn limit before finishing)" end private # System messages owed on this turn: an exit notice, the full mode text on entry, # or a one-line refresher every TURNS_BETWEEN_REFRESHERS user turns. def due_system_messages due = [] if @exit_pending @exit_pending = false @mode_announced = false due << {role: "system", content: MODE_EXIT} end if @mode_on if !@mode_announced @mode_announced = true @turns_since_reminder = 0 due << {role: "system", content: MODE_ENTER} elsif @turns_since_reminder >= TURNS_BETWEEN_REFRESHERS @turns_since_reminder = 0 due << {role: "system", content: MODE_REFRESH} end end due end end ``` ## Run it The bash tool in this example runs model-written commands directly on your machine with no sandbox, and the fan-out runs several of those agents in parallel. Run it in a directory and environment you are comfortable exposing, and add sandboxing before adapting it for anything beyond local experimentation. ```python Python if __name__ == "__main__": task = ( sys.argv[1] if len(sys.argv) > 1 else "Explore the current directory, then give a thorough review: what it does, " "code-quality issues, and concrete improvements." ) agent = ModeAgent(MODEL) print(agent.turn(task)) agent.set_mode(False) print(agent.turn("Briefly summarize what you found above, no fan-out needed.")) ``` ```typescript TypeScript const task = process.argv[2] ?? "Explore the current directory, then give a thorough review: what it does, " + "code-quality issues, and concrete improvements."; const agent = new ModeAgent(MODEL); console.log(await agent.turn(task)); agent.setMode(false); console.log(await agent.turn("Briefly summarize what you found above, no fan-out needed.")); ``` ```csharp C# var task = args.Length > 0 ? args[0] : "Explore the current directory, then give a thorough review: what it does, " + "code-quality issues, and concrete improvements."; Console.WriteLine(await Turn(task)); SetMode(false); Console.WriteLine(await Turn("Briefly summarize what you found above, no fan-out needed.")); ``` ```go Go func main() { if err := run(context.Background()); err != nil { log.Fatal(err) } } func run(ctx context.Context) error { if docTestMode { defer os.RemoveAll(workDir) } task := "Explore the current directory, then give a thorough review: what it does, " + "code-quality issues, and concrete improvements." if len(os.Args) > 1 { task = os.Args[1] } agent := newModeAgent(modelID) answer, err := agent.turn(ctx, task) if err != nil { return err } fmt.Println(answer) agent.setMode(false) summary, err := agent.turn(ctx, "Briefly summarize what you found above, no fan-out needed.") if err != nil { return err } fmt.Println(summary) return nil } ``` ```java Java void main(String[] args) throws InterruptedException { String task = args.length > 0 ? args[0] : "Explore the current directory, then give a thorough review: what it does, " + "code-quality issues, and concrete improvements."; ModeAgent agent = new ModeAgent(MODEL); IO.println(agent.turn(task)); agent.setMode(false); IO.println(agent.turn("Briefly summarize what you found above, no fan-out needed.")); } ``` ```php PHP $task = $argv[1] ?? 'Explore the current directory, then give a thorough review: what it does, ' . 'code-quality issues, and concrete improvements.'; $agent = new ModeAgent($client, MODEL); echo $agent->turn($task), PHP_EOL; $agent->setMode(false); echo $agent->turn('Briefly summarize what you found above, no fan-out needed.'), PHP_EOL; ``` ```ruby Ruby task = ARGV[0] || "Explore the current directory, then give a thorough review: what it does, " \ "code-quality issues, and concrete improvements." agent = ModeAgent.new(MODEL) puts agent.turn(task) agent.set_mode(false) puts agent.turn("Briefly summarize what you found above, no fan-out needed.") ``` Start the example from the directory you want the agents to work in, for example the root of a repository to review: ```bash python orchestration_mode.py "Review this repository for flaky tests and propose fixes." ``` With the mode on, expect the model to scout with a few bash commands, dispatch the Workflow tool unprompted, and synthesize the subagent reports into a final answer. Trivial or conversational requests stay solo, as the reminder instructs. ## Toward a production harness This example is deliberately small. A harness meant for real workloads would typically add: * **Sandboxed orchestration scripts:** let the model emit a short orchestration program (branching, loops, and reduce steps) and run it inside an isolated interpreter, rather than accepting only a flat list of subtask strings. * **Durable journaling:** replace the local JSON file with a store that survives process restarts and is safe under concurrent writers across machines. * **Budget enforcement:** track total subagents launched across the whole session, not just per Workflow call, and refuse to exceed a hard cap so a runaway plan cannot exhaust your quota. The patterns in this example (the mode reminders, standing consent in the tool description, journaling, and a verification wave) carry over unchanged; only the execution substrate around them gets more robust. ## Related The mechanism the mode reminders use, and how it interacts with prompt caching. The effort levels the API accepts and how to choose one. Defining tools, handling tool calls, and tool results. The Anthropic-defined bash tool this example executes locally. --- title: Cache diagnostics url: https://platform.claude.com/docs/en/build-with-claude/cache-diagnostics description: Diagnose unexpected prompt cache misses by comparing consecutive requests and identifying exactly where the prompt prefix diverged. --- ## Compatibility - Status: Beta - [Beta header](https://platform.claude.com/docs/en/api/beta-headers): `cache-diagnosis-2026-04-07` - [ZDR](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention): eligible (excludes [Covered Models](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#model-specific-data-retention-requirements)) - Platforms: Claude API (beta); not available on Claude Platform on AWS, Amazon Bedrock, Google Cloud, Microsoft Foundry [Prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) cuts latency and cost significantly, but only when the beginning of your prompt is byte-for-byte identical to a recent request. A reordered tool, a timestamp interpolated into your system prompt, or an edit to an earlier message can silently invalidate the cache. Without cache diagnostics, the only signal is `usage.cache_read_input_tokens` dropping to zero, with no indication of what changed. Cache diagnostics closes that gap. Pass the `id` of your previous response, and the API compares the two requests and tells you where they diverged (the model, the system prompt, the tools, or the message history) so you can fix the root cause instead of guessing. ## How cache diagnostics works When the beta header is present, the API stores a lightweight fingerprint of each request, keyed by the response `id`. On your next request, include that `id` as `diagnostics.previous_message_id`. The API rebuilds the fingerprint for the new request, compares it against the stored one, and attaches a `diagnostics` object to the response describing the first point of divergence. The comparison is about request structure, independent of whether the cache actually hit. See [Reading diagnostics alongside usage](https://platform.claude.com/docs/en/build-with-claude/cache-diagnostics#reading-diagnostics-alongside-usage) for how to combine the `diagnostics` result with `usage.cache_read_input_tokens`. Fingerprints contain only hashes and token-count estimates (never raw prompt content), are retained for a limited time, are scoped to your organization and workspace, and are not used for any other purpose. ## Basic usage Send the beta header on every turn. On the first turn, pass `"previous_message_id": null` to opt in without a prior message to compare against. On subsequent turns, pass the `id` from the previous response. ```bash cURL # Turn 1: establish the cache and opt in to diagnostics response=$(curl -sS --fail-with-body https://api.anthropic.com/v1/messages \ --header "x-api-key: $ANTHROPIC_API_KEY" \ --header "anthropic-version: 2023-06-01" \ --header "anthropic-beta: cache-diagnosis-2026-04-07" \ --header "content-type: application/json" \ --data '{ "model": "claude-opus-5", "max_tokens": 1024, "cache_control": {"type": "ephemeral"}, "system": "You are an AI assistant analyzing a large document. ...", "messages": [{"role": "user", "content": "Summarize section 1."}], "diagnostics": {"previous_message_id": null} }') jq '{id, diagnostics}' <<< "$response" message_id=$(jq -r '.id' <<< "$response") # Turn 2: reference the previous turn so the API can compare prefixes curl -sS --fail-with-body https://api.anthropic.com/v1/messages \ --header "x-api-key: $ANTHROPIC_API_KEY" \ --header "anthropic-version: 2023-06-01" \ --header "anthropic-beta: cache-diagnosis-2026-04-07" \ --header "content-type: application/json" \ --data @- <...", "messages": [ {"role": "user", "content": "Summarize section 1."}, {"role": "assistant", "content": "Section 1 covers..."}, {"role": "user", "content": "Now summarize section 2."} ], "diagnostics": {"previous_message_id": "$message_id"} } EOF ``` ```bash CLI # Turn 1 turn1=$(ant beta:messages create \ --beta cache-diagnosis-2026-04-07 \ --transform '{id,usage,diagnostics}' <<'YAML' model: claude-opus-5 max_tokens: 1024 cache_control: type: ephemeral system: "You are an AI assistant analyzing a large document. ..." messages: - role: user content: Summarize section 1. diagnostics: previous_message_id: null YAML ) printf '%s\n' "$turn1" # Turn 2: pass the id from turn 1 as previous_message_id message_id=$(jq -r '.id' <<<"$turn1") ant beta:messages create \ --beta cache-diagnosis-2026-04-07 \ --transform '{id,usage,diagnostics}' <..." messages: - role: user content: Summarize section 1. - role: assistant content: Section 1 covers... - role: user content: Now summarize section 2. diagnostics: previous_message_id: $message_id YAML ``` ```python Python client = anthropic.Anthropic() SYSTEM = "You are an AI assistant analyzing a large document. ..." # Turn 1: opt in with previous_message_id=None r1 = client.beta.messages.create( model="claude-opus-5", max_tokens=1024, cache_control={"type": "ephemeral"}, system=SYSTEM, messages=[{"role": "user", "content": "Summarize section 1."}], diagnostics={"previous_message_id": None}, betas=["cache-diagnosis-2026-04-07"], ) # Turn 2: reference the previous response id r2 = client.beta.messages.create( model="claude-opus-5", max_tokens=1024, cache_control={"type": "ephemeral"}, system=SYSTEM, messages=[ {"role": "user", "content": "Summarize section 1."}, {"role": "assistant", "content": r1.content}, {"role": "user", "content": "Now summarize section 2."}, ], diagnostics={"previous_message_id": r1.id}, betas=["cache-diagnosis-2026-04-07"], ) diagnostics = r2.diagnostics if diagnostics is None: print("No divergence detected.") elif diagnostics.cache_miss_reason is None: print("Comparison still pending.") else: print(f"cache_miss_reason: {diagnostics.cache_miss_reason.type}") ``` ```typescript TypeScript const client = new Anthropic(); const SYSTEM = "You are an AI assistant analyzing a large document. ..."; // Turn 1: opt in with previous_message_id: null const r1 = await client.beta.messages.create({ model: "claude-opus-5", max_tokens: 1024, cache_control: { type: "ephemeral" }, system: SYSTEM, messages: [{ role: "user", content: "Summarize section 1." }], diagnostics: { previous_message_id: null }, betas: ["cache-diagnosis-2026-04-07"] }); // Turn 2: reference the previous response id const r2 = await client.beta.messages.create({ model: "claude-opus-5", max_tokens: 1024, cache_control: { type: "ephemeral" }, system: SYSTEM, messages: [ { role: "user", content: "Summarize section 1." }, { role: "assistant", content: r1.content }, { role: "user", content: "Now summarize section 2." } ], diagnostics: { previous_message_id: r1.id }, betas: ["cache-diagnosis-2026-04-07"] }); if (r2.diagnostics === null) { console.log("No divergence detected."); } else if (r2.diagnostics.cache_miss_reason === null) { console.log("Comparison still pending."); } else { console.log(`cache_miss_reason: ${r2.diagnostics.cache_miss_reason.type}`); } ``` ```csharp C# AnthropicClient client = new(); var system = "You are an AI assistant analyzing a large document. ..."; var r1 = await client.Beta.Messages.Create( new() { Model = Messages::Model.ClaudeOpus5, MaxTokens = 1024, CacheControl = new(), System = system, Messages = [ new() { Role = Role.User, Content = "Summarize section 1." }, ], Diagnostics = new() { PreviousMessageID = null }, Betas = [AnthropicBeta.CacheDiagnosis2026_04_07], } ); var r2 = await client.Beta.Messages.Create( new() { Model = Messages::Model.ClaudeOpus5, MaxTokens = 1024, CacheControl = new(), System = system, Messages = [ new() { Role = Role.User, Content = "Summarize section 1." }, new() { Role = Role.Assistant, Content = r1.Content.Select(block => new BetaContentBlockParam(block.Json)).ToList(), }, new() { Role = Role.User, Content = "Now summarize section 2." }, ], Diagnostics = new() { PreviousMessageID = r1.ID }, Betas = [AnthropicBeta.CacheDiagnosis2026_04_07], } ); Console.WriteLine(r2.Diagnostics switch { null => "No divergence detected.", { CacheMissReason: null } => "Comparison still pending.", { CacheMissReason.Type: var type } => $"cache_miss_reason: {type.GetString()}", }); ``` ```go Go client := anthropic.NewClient() ctx := context.Background() system := []anthropic.BetaTextBlockParam{ {Text: "You are an AI assistant analyzing a large document. ..."}, } r1, err := client.Beta.Messages.New(ctx, anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, CacheControl: anthropic.BetaCacheControlEphemeralParam{}, System: system, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Summarize section 1.")), }, Diagnostics: anthropic.BetaDiagnosticsParam{ PreviousMessageID: param.Null[string](), }, Betas: []anthropic.AnthropicBeta{anthropic.AnthropicBetaCacheDiagnosis2026_04_07}, }) if err != nil { panic(err) } r2, err := client.Beta.Messages.New(ctx, anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, CacheControl: anthropic.BetaCacheControlEphemeralParam{}, System: system, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Summarize section 1.")), r1.ToParam(), anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Now summarize section 2.")), }, Diagnostics: anthropic.BetaDiagnosticsParam{ PreviousMessageID: anthropic.String(r1.ID), }, Betas: []anthropic.AnthropicBeta{anthropic.AnthropicBetaCacheDiagnosis2026_04_07}, }) if err != nil { panic(err) } switch { case !r2.JSON.Diagnostics.Valid(): fmt.Println("No divergence detected.") case !r2.Diagnostics.JSON.CacheMissReason.Valid(): fmt.Println("Comparison still pending.") default: fmt.Printf("cache_miss_reason: %s\n", r2.Diagnostics.CacheMissReason.Type) } ``` ```java Java var client = AnthropicOkHttpClient.fromEnv(); var system = "You are an AI assistant analyzing a large document. ..."; var r1 = client.beta().messages().create( MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .cacheControl(BetaCacheControlEphemeral.builder().build()) .system(system) .addUserMessage("Summarize section 1.") // Pass null on the first turn to opt in without a prior message to compare. .diagnostics(BetaDiagnosticsParam.builder().previousMessageId((String) null).build()) .addBeta(AnthropicBeta.CACHE_DIAGNOSIS_2026_04_07) .build() ); var r2 = client.beta().messages().create( MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .cacheControl(BetaCacheControlEphemeral.builder().build()) .system(system) .addUserMessage("Summarize section 1.") .addMessage(r1) .addUserMessage("Now summarize section 2.") .diagnostics(BetaDiagnosticsParam.builder().previousMessageId(r1.id()).build()) .addBeta(AnthropicBeta.CACHE_DIAGNOSIS_2026_04_07) .build() ); if (r2.diagnostics().isEmpty()) { IO.println("No divergence detected."); } else if (r2.diagnostics().get().cacheMissReason().isEmpty()) { IO.println("Comparison still pending."); } else { var reason = r2.diagnostics().get().cacheMissReason().get(); // CacheMissReason doesn't expose a typed .type() accessor; read it from the raw JSON. @SuppressWarnings("unchecked") var json = (Map) reason._json().orElseThrow().asObject().orElseThrow(); IO.println("cache_miss_reason: " + json.get("type").asStringOrThrow()); } ``` ```php PHP $client = new Client(); $system = 'You are an AI assistant analyzing a large document. ...'; $r1 = $client->beta->messages->create( model: Model::CLAUDE_OPUS_5, maxTokens: 1024, cacheControl: new BetaCacheControlEphemeral, system: $system, messages: [ ['role' => 'user', 'content' => 'Summarize section 1.'], ], diagnostics: (new BetaDiagnosticsParam)->withPreviousMessageID(null), betas: [AnthropicBeta::CACHE_DIAGNOSIS_2026_04_07], ); $r2 = $client->beta->messages->create( model: Model::CLAUDE_OPUS_5, maxTokens: 1024, cacheControl: new BetaCacheControlEphemeral, system: $system, messages: [ ['role' => 'user', 'content' => 'Summarize section 1.'], ['role' => 'assistant', 'content' => $r1->content], ['role' => 'user', 'content' => 'Now summarize section 2.'], ], diagnostics: (new BetaDiagnosticsParam)->withPreviousMessageID($r1->id), betas: [AnthropicBeta::CACHE_DIAGNOSIS_2026_04_07], ); echo match (true) { $r2->diagnostics === null => "No divergence detected.\n", $r2->diagnostics->cacheMissReason === null => "Comparison still pending.\n", default => "cache_miss_reason: {$r2->diagnostics->cacheMissReason->type}\n", }; ``` ```ruby Ruby client = Anthropic::Client.new SYSTEM = "You are an AI assistant analyzing a large document. ..." r1 = client.beta.messages.create( model: :"claude-opus-5", max_tokens: 1024, cache_control: {type: "ephemeral"}, system_: SYSTEM, messages: [ {role: "user", content: "Summarize section 1."} ], diagnostics: {previous_message_id: nil}, betas: ["cache-diagnosis-2026-04-07"] ) r2 = client.beta.messages.create( model: :"claude-opus-5", max_tokens: 1024, cache_control: {type: "ephemeral"}, system_: SYSTEM, messages: [ {role: "user", content: "Summarize section 1."}, {role: "assistant", content: r1.content}, {role: "user", content: "Now summarize section 2."} ], diagnostics: {previous_message_id: r1.id}, betas: ["cache-diagnosis-2026-04-07"] ) case r2.diagnostics in nil puts "No divergence detected." in {cache_miss_reason: nil} puts "Comparison still pending." in {cache_miss_reason: {type:}} puts "cache_miss_reason: #{type}" end ``` ## Streaming In streaming responses, `diagnostics` appears on the `message_start` event. ```bash cURL # Turn 2: stream the response. diagnostics arrives on the message_start event; # a null value means no divergence was found. curl -sS --fail-with-body https://api.anthropic.com/v1/messages \ --header "x-api-key: $ANTHROPIC_API_KEY" \ --header "anthropic-version: 2023-06-01" \ --header "anthropic-beta: cache-diagnosis-2026-04-07" \ --header "content-type: application/json" \ --data @- <...", "messages": [ {"role": "user", "content": "Summarize section 1."}, {"role": "assistant", "content": "Section 1 covers..."}, {"role": "user", "content": "Now summarize section 2."} ], "diagnostics": {"previous_message_id": "$message_id"} } EOF ``` ```bash CLI # Turn 2: stream. With --stream the CLI emits each SSE event as one JSON object. # diagnostics arrives on the message_start event; pick it out with jq. ant beta:messages create \ --beta cache-diagnosis-2026-04-07 \ --stream --format jsonl <..." messages: - role: user content: Summarize section 1. - role: assistant content: Section 1 covers... - role: user content: Now summarize section 2. diagnostics: previous_message_id: $message_id YAML jq -c 'select(.type == "message_start") | .message | {id,usage,diagnostics}' ``` ```python Python # Turn 2: stream, referencing the previous response id with client.beta.messages.stream( model="claude-opus-5", max_tokens=1024, cache_control={"type": "ephemeral"}, system=SYSTEM, messages=[ {"role": "user", "content": "Summarize section 1."}, {"role": "assistant", "content": r1.content}, {"role": "user", "content": "Now summarize section 2."}, ], diagnostics={"previous_message_id": r1.id}, betas=["cache-diagnosis-2026-04-07"], ) as stream: for text in stream.text_stream: print(text, end="", flush=True) print() r2 = stream.get_final_message() diagnostics = r2.diagnostics if diagnostics is None: print("No divergence detected.") elif diagnostics.cache_miss_reason is None: print("Comparison still pending.") else: print(f"cache_miss_reason: {diagnostics.cache_miss_reason.type}") ``` ```typescript TypeScript const stream = client.beta.messages.stream({ model: "claude-opus-5", max_tokens: 1024, cache_control: { type: "ephemeral" }, system: SYSTEM, messages: [ { role: "user", content: "Summarize section 1." }, { role: "assistant", content: r1.content }, { role: "user", content: "Now summarize section 2." } ], diagnostics: { previous_message_id: r1.id }, betas: ["cache-diagnosis-2026-04-07"] }); for await (const event of stream) { if (event.type === "content_block_delta" && event.delta.type === "text_delta") { process.stdout.write(event.delta.text); } } process.stdout.write("\n"); // diagnostics arrives on message_start and is carried through to the final message const r2 = await stream.finalMessage(); if (r2.diagnostics === null) { console.log("No divergence detected."); } else if (r2.diagnostics.cache_miss_reason === null) { console.log("Comparison still pending."); } else { console.log(`cache_miss_reason: ${r2.diagnostics.cache_miss_reason.type}`); } ``` ```csharp C# // Turn 2: stream, referencing the previous response id BetaDiagnostics? diagnostics = null; var stream = client.Beta.Messages.CreateStreaming( new() { Model = Messages::Model.ClaudeOpus5, MaxTokens = 1024, CacheControl = new(), System = system, Messages = [ new() { Role = Role.User, Content = "Summarize section 1." }, new() { Role = Role.Assistant, Content = r1.Content.Select(block => new BetaContentBlockParam(block.Json)).ToList(), }, new() { Role = Role.User, Content = "Now summarize section 2." }, ], Diagnostics = new() { PreviousMessageID = r1.ID }, Betas = [AnthropicBeta.CacheDiagnosis2026_04_07], } ); await foreach (var streamEvent in stream) { if (streamEvent.TryPickStart(out var start)) { // diagnostics arrives on the message_start event diagnostics = start.Message.Diagnostics; } else if (streamEvent.TryPickContentBlockDelta(out var delta) && delta.Delta.TryPickText(out var textDelta)) { Console.Write(textDelta.Text); } } Console.WriteLine(); Console.WriteLine(diagnostics switch { null => "No divergence detected.", { CacheMissReason: null } => "Comparison still pending.", { CacheMissReason.Type: var type } => $"cache_miss_reason: {type.GetString()}", }); ``` ```go Go // Turn 2: stream, referencing the previous response id stream := client.Beta.Messages.NewStreaming(ctx, anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, CacheControl: anthropic.BetaCacheControlEphemeralParam{}, System: system, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Summarize section 1.")), r1.ToParam(), anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Now summarize section 2.")), }, Diagnostics: anthropic.BetaDiagnosticsParam{ PreviousMessageID: anthropic.String(r1.ID), }, Betas: []anthropic.AnthropicBeta{anthropic.AnthropicBetaCacheDiagnosis2026_04_07}, }) defer stream.Close() // diagnostics arrives on message_start; Accumulate carries it into r2 var r2 anthropic.BetaMessage for stream.Next() { if err := r2.Accumulate(stream.Current()); err != nil { panic(err) } } if err := stream.Err(); err != nil { panic(err) } switch { case !r2.JSON.Diagnostics.Valid(): fmt.Println("No divergence detected.") case !r2.Diagnostics.JSON.CacheMissReason.Valid(): fmt.Println("Comparison still pending.") default: fmt.Printf("cache_miss_reason: %s\n", r2.Diagnostics.CacheMissReason.Type) } ``` ```java Java // Turn 2: stream, referencing the previous response id var params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .cacheControl(BetaCacheControlEphemeral.builder().build()) .system(system) .addUserMessage("Summarize section 1.") .addMessage(r1) .addUserMessage("Now summarize section 2.") .diagnostics(BetaDiagnosticsParam.builder().previousMessageId(r1.id()).build()) .addBeta(AnthropicBeta.CACHE_DIAGNOSIS_2026_04_07) .build(); var accumulator = BetaMessageAccumulator.create(); try (var streamResponse = client.beta().messages().createStreaming(params)) { streamResponse.stream() .peek(accumulator::accumulate) .flatMap(event -> event.contentBlockDelta().stream()) .flatMap(deltaEvent -> deltaEvent.delta().text().stream()) .forEach(textDelta -> IO.print(textDelta.text())); IO.println(""); } // diagnostics arrives on message_start and is carried through to the accumulated message var diagnostics = accumulator.message().diagnostics(); if (diagnostics.isEmpty()) { IO.println("No divergence detected."); } else if (diagnostics.get().cacheMissReason().isEmpty()) { IO.println("Comparison still pending."); } else { var reason = diagnostics.get().cacheMissReason().get(); // CacheMissReason doesn't expose a typed .type() accessor; read it from the raw JSON. @SuppressWarnings("unchecked") var json = (Map) reason._json().orElseThrow().asObject().orElseThrow(); IO.println("cache_miss_reason: " + json.get("type").asStringOrThrow()); } ``` ```php PHP // Turn 2: stream, referencing the previous response id $stream = $client->beta->messages->createStream( model: Model::CLAUDE_OPUS_5, maxTokens: 1024, cacheControl: new BetaCacheControlEphemeral, system: $system, messages: [ ['role' => 'user', 'content' => 'Summarize section 1.'], ['role' => 'assistant', 'content' => $r1->content], ['role' => 'user', 'content' => 'Now summarize section 2.'], ], diagnostics: (new BetaDiagnosticsParam)->withPreviousMessageID($r1->id), betas: [AnthropicBeta::CACHE_DIAGNOSIS_2026_04_07], ); $diagnostics = null; foreach ($stream as $event) { if ($event instanceof BetaRawMessageStartEvent) { // diagnostics arrives on the message_start event's embedded BetaMessage $diagnostics = $event->message->diagnostics; } elseif ($event instanceof BetaRawContentBlockDeltaEvent && $event->delta instanceof BetaTextDelta) { echo $event->delta->text; } } echo PHP_EOL; echo match (true) { $diagnostics === null => "No divergence detected.\n", $diagnostics->cacheMissReason === null => "Comparison still pending.\n", default => "cache_miss_reason: {$diagnostics->cacheMissReason->type}\n", }; ``` ```ruby Ruby # Turn 2: stream, referencing the previous response id stream = client.beta.messages.stream( model: :"claude-opus-5", max_tokens: 1024, cache_control: {type: "ephemeral"}, system_: SYSTEM, messages: [ {role: "user", content: "Summarize section 1."}, {role: "assistant", content: r1.content}, {role: "user", content: "Now summarize section 2."} ], diagnostics: {previous_message_id: r1.id}, betas: ["cache-diagnosis-2026-04-07"] ) stream.each do |event| print(event.text) if event.is_a?(Anthropic::Streaming::TextEvent) end puts # diagnostics arrives on message_start and is retained on the accumulated message r2 = stream.accumulated_message case r2.diagnostics in nil puts "No divergence detected." in {cache_miss_reason: nil} puts "Comparison still pending." in {cache_miss_reason: {type:}} puts "cache_miss_reason: #{type}" end ``` The `message_start` event carries the full `diagnostics` field; see [Response format](https://platform.claude.com/docs/en/build-with-claude/cache-diagnostics#response-format) for the possible values. ## Threading diagnostics through a conversation loop In a multi-turn conversation, carry the latest response `id` forward as `previous_message_id` on every turn. The first iteration passes `null` to opt in; each subsequent iteration passes the `id` from the previous response. This workflow doesn't translate well to a one-off shell command. See the SDK tabs for the loop pattern; the per-turn HTTP request is identical to [Basic usage](https://platform.claude.com/docs/en/build-with-claude/cache-diagnostics#basic-usage). This workflow doesn't translate well to a one-off shell command. See the SDK tabs for the loop pattern; the per-turn CLI invocation is identical to [Basic usage](https://platform.claude.com/docs/en/build-with-claude/cache-diagnostics#basic-usage). ```python client = anthropic.Anthropic() SYSTEM = "You are an AI assistant analyzing a large document. ..." messages = [] prev_id = None for i, user_message in enumerate( ["Summarize section 1.", "Now section 2.", "Now section 3."] ): messages.append({"role": "user", "content": user_message}) r = client.beta.messages.create( model="claude-opus-5", max_tokens=1024, cache_control={"type": "ephemeral"}, system=SYSTEM, messages=messages, diagnostics={"previous_message_id": prev_id}, betas=["cache-diagnosis-2026-04-07"], ) if r.diagnostics is not None and r.diagnostics.cache_miss_reason is not None: print(f"Turn {i + 1} cache_miss_reason: {r.diagnostics.cache_miss_reason.type}") messages.append({"role": "assistant", "content": r.content}) prev_id = r.id ``` ```typescript const client = new Anthropic(); const SYSTEM = "You are an AI assistant analyzing a large document. ..."; const prompts = ["Summarize section 1.", "Now section 2.", "Now section 3."]; const messages: BetaMessageParam[] = []; let prevId: string | null = null; for (const [i, prompt] of prompts.entries()) { messages.push({ role: "user", content: prompt }); const r: BetaMessage = await client.beta.messages.create({ model: "claude-opus-5", max_tokens: 1024, cache_control: { type: "ephemeral" }, system: SYSTEM, messages, diagnostics: { previous_message_id: prevId }, betas: ["cache-diagnosis-2026-04-07"] }); if (r.diagnostics?.cache_miss_reason) { console.log(`Turn ${i + 1} cache_miss_reason: ${r.diagnostics.cache_miss_reason.type}`); } messages.push({ role: "assistant", content: r.content }); prevId = r.id; } ``` ```csharp AnthropicClient client = new(); var system = "You are an AI assistant analyzing a large document. ..."; List messages = []; string? prevId = null; string[] prompts = ["Summarize section 1.", "Now section 2.", "Now section 3."]; for (int i = 0; i < prompts.Length; i++) { messages.Add(new() { Role = Role.User, Content = prompts[i] }); var r = await client.Beta.Messages.Create( new() { Model = Messages::Model.ClaudeOpus5, MaxTokens = 1024, CacheControl = new(), System = system, Messages = messages, Diagnostics = new() { PreviousMessageID = prevId }, Betas = [AnthropicBeta.CacheDiagnosis2026_04_07], } ); if (r.Diagnostics?.CacheMissReason is { Type: var type }) { Console.WriteLine($"Turn {i + 1} cache_miss_reason: {type.GetString()}"); } messages.Add( new() { Role = Role.Assistant, Content = r.Content.Select(block => new BetaContentBlockParam(block.Json)).ToList(), } ); prevId = r.ID; } ``` ```go client := anthropic.NewClient() ctx := context.Background() system := []anthropic.BetaTextBlockParam{ {Text: "You are an AI assistant analyzing a large document. ..."}, } prompts := []string{"Summarize section 1.", "Now section 2.", "Now section 3."} var messages []anthropic.BetaMessageParam prevID := param.Null[string]() for turn, prompt := range prompts { messages = append(messages, anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock(prompt))) r, err := client.Beta.Messages.New(ctx, anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, CacheControl: anthropic.BetaCacheControlEphemeralParam{}, System: system, Messages: messages, Diagnostics: anthropic.BetaDiagnosticsParam{ PreviousMessageID: prevID, }, Betas: []anthropic.AnthropicBeta{anthropic.AnthropicBetaCacheDiagnosis2026_04_07}, }) if err != nil { panic(err) } if r.JSON.Diagnostics.Valid() && r.Diagnostics.JSON.CacheMissReason.Valid() { fmt.Printf("Turn %d cache_miss_reason: %s\n", turn+1, r.Diagnostics.CacheMissReason.Type) } messages = append(messages, r.ToParam()) prevID = anthropic.String(r.ID) } ``` ```java var client = AnthropicOkHttpClient.fromEnv(); var system = "You are an AI assistant analyzing a large document. ..."; var prompts = List.of("Summarize section 1.", "Now section 2.", "Now section 3."); var messages = new ArrayList(); String prevId = null; for (var turn = 0; turn < prompts.size(); turn++) { messages.add( BetaMessageParam.builder() .role(BetaMessageParam.Role.USER) .content(prompts.get(turn)) .build() ); var r = client.beta().messages().create( MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .cacheControl(BetaCacheControlEphemeral.builder().build()) .system(system) .messages(messages) .diagnostics(BetaDiagnosticsParam.builder().previousMessageId(prevId).build()) .addBeta(AnthropicBeta.CACHE_DIAGNOSIS_2026_04_07) .build() ); if (r.diagnostics().isPresent() && r.diagnostics().get().cacheMissReason().isPresent()) { var reason = r.diagnostics().get().cacheMissReason().get(); // CacheMissReason doesn't expose a typed .type() accessor; read it from the raw JSON. @SuppressWarnings("unchecked") var json = (Map) reason._json().orElseThrow().asObject().orElseThrow(); IO.println("Turn " + (turn + 1) + " cache_miss_reason: " + json.get("type").asStringOrThrow()); } messages.add(r.toParam()); prevId = r.id(); } ``` ```php $client = new Client(); $system = 'You are an AI assistant analyzing a large document. ...'; $messages = []; $prevId = null; foreach (['Summarize section 1.', 'Now section 2.', 'Now section 3.'] as $i => $userMsg) { $turn = $i + 1; $messages[] = ['role' => 'user', 'content' => $userMsg]; $r = $client->beta->messages->create( model: Model::CLAUDE_OPUS_5, maxTokens: 1024, cacheControl: new BetaCacheControlEphemeral, system: $system, messages: $messages, diagnostics: (new BetaDiagnosticsParam)->withPreviousMessageID($prevId), betas: [AnthropicBeta::CACHE_DIAGNOSIS_2026_04_07], ); if ($r->diagnostics?->cacheMissReason !== null) { echo "Turn {$turn} cache_miss_reason: {$r->diagnostics->cacheMissReason->type}\n"; } $messages[] = ['role' => 'assistant', 'content' => $r->content]; $prevId = $r->id; } ``` ```ruby client = Anthropic::Client.new SYSTEM = "You are an AI assistant analyzing a large document. ..." messages = [] prev_id = nil ["Summarize section 1.", "Now section 2.", "Now section 3."].each_with_index do |user_msg, i| messages << {role: "user", content: user_msg} r = client.beta.messages.create( model: :"claude-opus-5", max_tokens: 1024, cache_control: {type: "ephemeral"}, system_: SYSTEM, messages: messages, diagnostics: {previous_message_id: prev_id}, betas: ["cache-diagnosis-2026-04-07"] ) if (reason = r.diagnostics&.cache_miss_reason) puts "Turn #{i + 1} cache_miss_reason: #{reason.type}" end messages << {role: "assistant", content: r.content} prev_id = r.id end ``` ## Response format The `diagnostics` field on the response `Message` has four possible states: | Value | Meaning | | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | field absent | The request did not include `diagnostics`, or the beta header was missing. | | `null` | Either `previous_message_id` was `null` (first turn, nothing to compare), or a comparison ran and found no divergence. | | `{"cache_miss_reason": null}` | The comparison was still running when the response was serialized. This can happen when the response starts very quickly. Treat it as inconclusive and check the next turn. | | `{"cache_miss_reason": {...}}` | A `cache_miss_reason` is attached. For `*_changed` types this identifies the first divergence point; `previous_message_not_found` and `unavailable` are cases where no comparison was produced. | When `cache_miss_reason` is non-null, it looks like this: ```json { "id": "msg_01Xyz...", "type": "message", "role": "assistant", "content": [{ "type": "text", "text": "..." }], "usage": { "input_tokens": 42, "cache_read_input_tokens": 0, "cache_creation_input_tokens": 41850, "output_tokens": 210 }, "diagnostics": { "cache_miss_reason": { "type": "system_changed", "cache_missed_input_tokens": 41850 } } } ``` ## Cache miss reason types `cache_miss_reason` is a discriminated union on `type`. The response reports the earliest divergence only, so fix it first; later ones may be hidden behind it. | Type | What it means | What to change | | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `model_changed` | The `model` differs from the previous request (for example, a router, A/B test, or fallback selected a different model). The cache is per-model. | Hold the model constant within a cached conversation. | | `system_changed` | The `system` parameter differs. Typically a timestamp, request ID, or other per-request value was interpolated into the system prompt. | Make the system prompt a byte-stable constant and move dynamic data into the first `user` message after your cache breakpoint. | | `tools_changed` | The `tools` array differs: tools were added, removed, or reordered between turns, or tool `input_schema` JSON was serialized non-deterministically. | Send the same tool list on every turn in a fixed order with deterministically serialized schemas (for example, sort keys). | | `messages_changed` | The model, system, and tools all match, but an earlier entry in `messages` was altered, reordered, or removed rather than appended to. Typically conversation history was truncated or edited, or assistant turns and `tool_result` blocks were re-serialized differently on resend. | Treat the history as append-only; echo assistant `content` and tool results back verbatim. | | `previous_message_not_found` | No stored fingerprint exists for the supplied `previous_message_id`. This is not evidence that your request changed. Typically the previous request did not carry the beta header, it came from a different workspace, or too much time has passed since it was sent. | Send the beta header on every turn and keep consecutive turns close together in time. | | `unavailable` | Diagnostic information was not available for this request. This includes the case where `model`, `system`, and `tools` match but another prompt-affecting request parameter (`tool_choice`, `thinking`, `context_management`, `output_config`, `output_format`, or the set of active `anthropic-beta` headers) differs, and very long conversations where the divergence is beyond the comparison horizon. Your request was processed normally. | Keep the prompt-affecting request parameters constant for the lifetime of a cached conversation. If persistent, apply the manual checks under [Troubleshooting common issues](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#troubleshooting-common-issues) on the prompt caching page. | The four `*_changed` types also carry a `cache_missed_input_tokens` integer: an estimate of how many input tokens fell after the divergence point, giving you a sense of how much cacheable prefix was lost. It is derived from byte lengths before tokenization, so treat it as a magnitude indicator rather than a billing number. It can differ from (and occasionally exceed) `usage.input_tokens`. ## Reading diagnostics alongside usage `diagnostics` answers "did my request change?" while `usage.cache_read_input_tokens` answers "did the cache hit?". Combining them tells you where to look. This matrix applies to turns where you passed a real `previous_message_id`. On the first turn (`previous_message_id: null`), `diagnostics` is always `null` and `cache_read_input_tokens` is normally zero because the cache is being written, not read; no troubleshooting is needed. The matrix also does not apply when `cache_miss_reason` is `null` (the comparison is still pending; check the next turn) or when its `type` is `previous_message_not_found` or `unavailable` (no comparison was produced). | Diagnostics result | Cache read tokens | Interpretation | | ----------------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `null` | high | Working as expected. Your prefix is stable and the cache hit. | | `null` | low or zero | Your requests match but the cache entry was no longer available. Consider shortening gaps between turns or using the [1-hour cache TTL](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#1-hour-cache-duration). | | `cache_miss_reason` is a `*_changed` type | low or zero | Your bug. The request changed; fix the cause indicated by `type`. | | `cache_miss_reason` is a `*_changed` type | high | Rare. A change occurred late in the prompt but an earlier `cache_control` breakpoint still hit. Worth fixing, but low impact. | ## Limitations * **Beta:** Field names and semantics may change before general availability. * **Claude API only:** Not available on Amazon Bedrock or Google Cloud. * **Limited retention:** Fingerprints for `previous_message_id` lookup expire after a short period. Run diagnostic comparisons between closely spaced requests. * **Same workspace:** The previous request must have been made with an API key from the same organization and workspace. To check, compare the `anthropic-workspace-id` [response header](https://platform.claude.com/docs/en/api/overview#response-headers) on the two responses. * **Comparison horizon:** For very long conversations where the only change is deep in the message list, the response may be `unavailable` rather than a precise location. * **Best-effort:** Diagnostics never blocks or fails your request. If diagnostic information is not available, the response returns `unavailable`, or `cache_miss_reason: null` when the comparison was still running. ## Data retention Cache diagnostics is ZDR eligible (qualified). Anthropic does not store the raw text of your prompts or Claude's outputs for this feature. The fingerprint stored for each request consists only of cryptographic hashes and token-count estimates, keyed by the response `id` and scoped to your organization and workspace. Fingerprints expire after a short period and are not used for any other purpose. For ZDR eligibility across all features, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention). ## See also * [Prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) * [Token counting](https://platform.claude.com/docs/en/build-with-claude/token-counting) * [Beta headers](https://platform.claude.com/docs/en/api/beta-headers) --- title: Compaction url: https://platform.claude.com/docs/en/build-with-claude/compaction description: Server-side context compaction for managing long conversations that approach context window limits. --- ## Compatibility - Status: Beta - [Beta header](https://platform.claude.com/docs/en/api/beta-headers): `compact-2026-01-12` - [ZDR](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention): eligible (excludes [Covered Models](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#model-specific-data-retention-requirements)) - Supported models: `claude-fable-5`, `claude-mythos-5`, `claude-mythos-preview`, `claude-opus-5`, `claude-opus-4-8`, `claude-opus-4-7`, `claude-opus-4-6`, `claude-sonnet-5`, `claude-sonnet-4-6` - Platforms: Claude API (beta), Claude Platform on AWS (beta), Amazon Bedrock (beta), Google Cloud (beta), Microsoft Foundry (beta) Server-side compaction is the recommended strategy for managing context in long-running conversations and agentic workflows. It handles context management automatically, without client-side summarization code. Compaction extends the effective context length for long-running conversations and tasks by automatically summarizing older context when approaching the context window limit. It also keeps the active context small: as a conversation grows, response quality degrades, so compaction replaces older content with a concise summary. For a deeper look at why long contexts degrade and how compaction helps, see [Effective context engineering](https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents). This is ideal for: * Chat-based, multi-turn conversations where you want users to use one chat for a long period of time * Task-oriented prompts that require a lot of follow-up work (often tool use) that might exceed the context window ## How compaction works When compaction is enabled, Claude automatically summarizes your conversation when it reaches the configured token threshold. The API: 1. Detects when input tokens reach your specified trigger threshold. 2. Generates a summary of the current conversation. 3. Creates a `compaction` block containing the summary. 4. Continues the response with the compacted context. On subsequent requests, append the response to your messages. The API automatically drops all content blocks prior to the `compaction` block, continuing the conversation from the summary. ![Compaction flow: when input tokens reach the trigger, Claude writes a summary into a compaction block and continues](https://platform.claude.com/docs/images/compaction-flow.svg) ## Basic usage Enable compaction by adding the `compact_20260112` strategy to `context_management.edits` in your Messages API request. ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: compact-2026-01-12" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 4096, "messages": [ { "role": "user", "content": "Help me build a website" } ], "context_management": { "edits": [ { "type": "compact_20260112" } ] } }' ``` ```bash CLI ant beta:messages create --beta compact-2026-01-12 <<'YAML' model: claude-opus-5 max_tokens: 4096 messages: - role: user content: Help me build a website context_management: edits: - type: compact_20260112 YAML ``` ```python Python client = anthropic.Anthropic() messages = [{"role": "user", "content": "Help me build a website"}] response = client.beta.messages.create( betas=["compact-2026-01-12"], model="claude-opus-5", max_tokens=4096, messages=messages, context_management={"edits": [{"type": "compact_20260112"}]}, ) # Append the response (including any compaction block) to continue the conversation messages.append({"role": "assistant", "content": response.content}) ``` ```typescript TypeScript const client = new Anthropic(); const messages: Anthropic.Beta.Messages.BetaMessageParam[] = [ { role: "user", content: "Help me build a website" } ]; const response = await client.beta.messages.create({ betas: ["compact-2026-01-12"], model: "claude-opus-5", max_tokens: 4096, messages, context_management: { edits: [ { type: "compact_20260112" } ] } }); // Append the response (including any compaction block) to continue the conversation messages.push({ role: "assistant", content: response.content }); ``` ```csharp C# AnthropicClient client = new(); var messages = new List { new() { Role = Role.User, Content = "Help me build a website" } }; var parameters = new MessageCreateParams { Betas = ["compact-2026-01-12"], Model = "claude-opus-5", MaxTokens = 4096, Messages = messages, ContextManagement = new BetaContextManagementConfig { Edits = [new BetaCompact20260112Edit()] } }; var response = await client.Beta.Messages.Create(parameters); // Append the response (including any compaction block) to continue the conversation messages.Add(new BetaMessageParam { Role = Role.Assistant, Content = response.Content.Select(block => new BetaContentBlockParam(block.Json)).ToList() }); Console.WriteLine(response); ``` ```go Go client := anthropic.NewClient() messages := []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Help me build a website")), } response, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 4096, Messages: messages, ContextManagement: anthropic.BetaContextManagementConfigParam{ Edits: []anthropic.BetaContextManagementConfigEditUnionParam{ {OfCompact20260112: &anthropic.BetaCompact20260112EditParam{}}, }, }, Betas: []anthropic.AnthropicBeta{"compact-2026-01-12"}, }) if err != nil { log.Fatal(err) } // Append the response (including any compaction block) to continue the conversation messages = append(messages, response.ToParam()) fmt.Println(response) ``` ```java Java import com.anthropic.models.beta.messages.BetaContextManagementConfig; import com.anthropic.models.beta.messages.BetaCompact20260112Edit; // ... AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .addBeta("compact-2026-01-12") .model("claude-opus-5") .maxTokens(4096L) .addUserMessage("Help me build a website") .contextManagement(BetaContextManagementConfig.builder() .addEdit(BetaCompact20260112Edit.builder().build()) .build()) .build(); BetaMessage response = client.beta().messages().create(params); // Append the response (including any compaction block) to continue the conversation // by including it in the next request's messages System.out.println(response); ``` ```php PHP $client = new Client(); $messages = [ ['role' => 'user', 'content' => 'Help me build a website'] ]; $response = $client->beta->messages->create( maxTokens: 4096, messages: $messages, model: 'claude-opus-5', betas: ['compact-2026-01-12'], contextManagement: [ 'edits' => [ ['type' => 'compact_20260112'] ] ] ); // Append the response (including any compaction block) to continue the conversation $messages[] = ['role' => 'assistant', 'content' => $response->content]; echo json_encode($response, JSON_PRETTY_PRINT), PHP_EOL; ``` ```ruby Ruby client = Anthropic::Client.new messages = [ { role: "user", content: "Help me build a website" } ] response = client.beta.messages.create( betas: ["compact-2026-01-12"], model: "claude-opus-5", max_tokens: 4096, messages: messages, context_management: { edits: [{ type: "compact_20260112" }] } ) # Append the response (including any compaction block) to continue the conversation messages << { role: "assistant", content: response.content } puts response ``` ## Parameters | Parameter | Type | Default | Description | | ------------------------ | ------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | `type` | string | Required | Must be `"compact_20260112"` | | `trigger` | object | `{"type": "input_tokens", "value": 150000}` | When to trigger compaction. `input_tokens` is the only supported trigger type. `value` must be at least 50,000 tokens. | | `pause_after_compaction` | boolean | `false` | Whether to pause after generating the compaction summary | | `instructions` | string | `null` | Custom summarization prompt. Completely replaces the default prompt when provided. | ### Trigger configuration Configure when compaction triggers using the `trigger` parameter: ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: compact-2026-01-12" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 4096, "messages": [ { "role": "user", "content": "Hello, Claude" } ], "context_management": { "edits": [ { "type": "compact_20260112", "trigger": { "type": "input_tokens", "value": 150000 } } ] } }' ``` ```bash CLI ant beta:messages create --beta compact-2026-01-12 <<'YAML' model: claude-opus-5 max_tokens: 4096 messages: - role: user content: Hello, Claude context_management: edits: - type: compact_20260112 trigger: type: input_tokens value: 150000 YAML ``` ```python Python client = anthropic.Anthropic() messages = [{"role": "user", "content": "Hello, Claude"}] response = client.beta.messages.create( betas=["compact-2026-01-12"], model="claude-opus-5", max_tokens=4096, messages=messages, context_management={ "edits": [ { "type": "compact_20260112", "trigger": {"type": "input_tokens", "value": 150000}, } ] }, ) ``` ```typescript TypeScript const client = new Anthropic(); const messages: Anthropic.Beta.Messages.BetaMessageParam[] = [ { role: "user", content: "Hello, Claude" } ]; const response = await client.beta.messages.create({ betas: ["compact-2026-01-12"], model: "claude-opus-5", max_tokens: 4096, messages, context_management: { edits: [ { type: "compact_20260112", trigger: { type: "input_tokens", value: 150000 } } ] } }); ``` ```csharp C# AnthropicClient client = new(); List messages = [new() { Role = Role.User, Content = "Hello" }]; var parameters = new MessageCreateParams { Model = "claude-opus-5", MaxTokens = 4096, Betas = ["compact-2026-01-12"], Messages = messages, ContextManagement = new BetaContextManagementConfig { Edits = [new BetaCompact20260112Edit { Trigger = new BetaInputTokensTrigger(150000) }] } }; var message = await client.Beta.Messages.Create(parameters); Console.WriteLine(message); ``` ```go Go client := anthropic.NewClient() messages := []anthropic.BetaMessageParam{anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Hello, Claude"))} response, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 4096, Messages: messages, ContextManagement: anthropic.BetaContextManagementConfigParam{ Edits: []anthropic.BetaContextManagementConfigEditUnionParam{ {OfCompact20260112: &anthropic.BetaCompact20260112EditParam{ Trigger: anthropic.BetaInputTokensTriggerParam{Value: 150000}, }}, }, }, Betas: []anthropic.AnthropicBeta{"compact-2026-01-12"}, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java import com.anthropic.models.beta.messages.BetaContextManagementConfig; import com.anthropic.models.beta.messages.BetaCompact20260112Edit; import com.anthropic.models.beta.messages.BetaInputTokensTrigger; // ... AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model("claude-opus-5") .maxTokens(4096L) .addBeta("compact-2026-01-12") .addUserMessage("Hello, Claude") .contextManagement(BetaContextManagementConfig.builder() .addEdit(BetaCompact20260112Edit.builder() .trigger(BetaInputTokensTrigger.builder() .value(150000L) .build()) .build()) .build()) .build(); BetaMessage response = client.beta().messages().create(params); System.out.println(response); ``` ```php PHP $client = new Client(); $messages = [['role' => 'user', 'content' => 'Hello, Claude']]; $message = $client->beta->messages->create( maxTokens: 4096, messages: $messages, model: 'claude-opus-5', betas: ['compact-2026-01-12'], contextManagement: [ 'edits' => [ [ 'type' => 'compact_20260112', 'trigger' => [ 'type' => 'input_tokens', 'value' => 150000 ] ] ] ] ); echo $message; ``` ```ruby Ruby client = Anthropic::Client.new messages = [{ role: "user", content: "Hello, Claude" }] response = client.beta.messages.create( betas: ["compact-2026-01-12"], model: "claude-opus-5", max_tokens: 4096, messages: messages, context_management: { edits: [ { type: "compact_20260112", trigger: { type: "input_tokens", value: 150000 } } ] } ) puts response ``` ### Custom summarization instructions The default summarization prompt varies by model. Each default instructs Claude to write a summary inside `` tags with the information needed to continue the task in a future context window. For example, some models use the following prompt: ```text wrap You have written a partial transcript for the initial task above. Please write a summary of the transcript. The purpose of this summary is to provide continuity so you can continue to make progress towards solving the task in a future context, where the raw history above may not be accessible and will be replaced with this summary. Write down anything that would be helpful, including the state, next steps, learnings etc. You must wrap your summary in a block. ``` You can provide custom instructions through the `instructions` parameter. Custom instructions don't supplement the default prompt. They replace it completely: ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: compact-2026-01-12" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 4096, "messages": [ { "role": "user", "content": "Hello, Claude" } ], "context_management": { "edits": [ { "type": "compact_20260112", "instructions": "Focus on preserving code snippets, variable names, and technical decisions." } ] } }' ``` ```bash CLI ant beta:messages create --beta compact-2026-01-12 <<'YAML' model: claude-opus-5 max_tokens: 4096 messages: - role: user content: Hello, Claude context_management: edits: - type: compact_20260112 instructions: >- Focus on preserving code snippets, variable names, and technical decisions. YAML ``` ```python Python client = anthropic.Anthropic() messages = [{"role": "user", "content": "Hello, Claude"}] response = client.beta.messages.create( betas=["compact-2026-01-12"], model="claude-opus-5", max_tokens=4096, messages=messages, context_management={ "edits": [ { "type": "compact_20260112", "instructions": "Focus on preserving code snippets, variable names, and technical decisions.", } ] }, ) ``` ```typescript TypeScript const client = new Anthropic(); const messages: Anthropic.Beta.Messages.BetaMessageParam[] = [ { role: "user", content: "Hello, Claude" } ]; const response = await client.beta.messages.create({ betas: ["compact-2026-01-12"], model: "claude-opus-5", max_tokens: 4096, messages, context_management: { edits: [ { type: "compact_20260112", instructions: "Focus on preserving code snippets, variable names, and technical decisions." } ] } }); ``` ```csharp C# AnthropicClient client = new(); var parameters = new MessageCreateParams { Betas = ["compact-2026-01-12"], Model = "claude-opus-5", MaxTokens = 4096, Messages = [ new BetaMessageParam { Role = Role.User, Content = "Help me build a Python web scraper" }, new BetaMessageParam { Role = Role.Assistant, Content = "I'll help you build a web scraper..." }, new BetaMessageParam { Role = Role.User, Content = "Add support for JavaScript-rendered pages" } ], ContextManagement = new BetaContextManagementConfig { Edits = [new BetaCompact20260112Edit { Instructions = "Focus on preserving code snippets, variable names, and technical decisions." }] } }; var message = await client.Beta.Messages.Create(parameters); Console.WriteLine(message); ``` ```go Go client := anthropic.NewClient() response, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 4096, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Help me build a Python web scraper")), {Role: anthropic.BetaMessageParamRoleAssistant, Content: []anthropic.BetaContentBlockParamUnion{anthropic.NewBetaTextBlock("I'll help you build a web scraper...")}}, anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Add support for JavaScript-rendered pages")), }, ContextManagement: anthropic.BetaContextManagementConfigParam{ Edits: []anthropic.BetaContextManagementConfigEditUnionParam{ {OfCompact20260112: &anthropic.BetaCompact20260112EditParam{ Instructions: anthropic.String("Focus on preserving code snippets, variable names, and technical decisions."), }}, }, }, Betas: []anthropic.AnthropicBeta{"compact-2026-01-12"}, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java import com.anthropic.models.beta.messages.BetaContextManagementConfig; import com.anthropic.models.beta.messages.BetaCompact20260112Edit; // ... AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .addBeta("compact-2026-01-12") .model("claude-opus-5") .maxTokens(4096L) .addUserMessage("Help me build a Python web scraper") .addAssistantMessage("I'll help you build a web scraper...") .addUserMessage("Add support for JavaScript-rendered pages") .contextManagement(BetaContextManagementConfig.builder() .addEdit(BetaCompact20260112Edit.builder() .instructions("Focus on preserving code snippets, variable names, and technical decisions.") .build()) .build()) .build(); BetaMessage response = client.beta().messages().create(params); System.out.println(response); ``` ```php PHP $client = new Client(); $response = $client->beta->messages->create( maxTokens: 4096, messages: [ ['role' => 'user', 'content' => 'Help me build a Python web scraper'], ['role' => 'assistant', 'content' => "I'll help you build a web scraper..."], ['role' => 'user', 'content' => 'Add support for JavaScript-rendered pages'] ], model: 'claude-opus-5', betas: ['compact-2026-01-12'], contextManagement: [ 'edits' => [ [ 'type' => 'compact_20260112', 'instructions' => 'Focus on preserving code snippets, variable names, and technical decisions.' ] ] ] ); echo json_encode($response, JSON_PRETTY_PRINT), PHP_EOL; ``` ```ruby Ruby client = Anthropic::Client.new response = client.beta.messages.create( betas: ["compact-2026-01-12"], model: "claude-opus-5", max_tokens: 4096, messages: [ { role: "user", content: "Help me build a Python web scraper" }, { role: "assistant", content: "I'll help you build a web scraper..." }, { role: "user", content: "Add support for JavaScript-rendered pages" } ], context_management: { edits: [ { type: "compact_20260112", instructions: "Focus on preserving code snippets, variable names, and technical decisions." } ] } ) puts response ``` ### Pausing after compaction Use `pause_after_compaction` to pause the API after generating the compaction summary. This allows you to add additional content blocks (such as preserving recent messages or specific instruction-oriented messages) before the API continues with the response. When enabled, the API returns a message with the `compaction` stop reason after generating the compaction block: ```bash cURL # pause_after_compaction stops the response right after the compaction # summary so you can adjust the messages before continuing. The continue # step doesn't translate well to a one-off shell command; see the SDK tabs # for the full pause-and-continue flow. Single paused request: curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: compact-2026-01-12" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 4096, "messages": [ { "role": "user", "content": "Hello, Claude" } ], "context_management": { "edits": [ { "type": "compact_20260112", "pause_after_compaction": true } ] } }' ``` ```bash CLI # pause_after_compaction stops the response right after the compaction # summary so you can adjust the messages before continuing. The continue # step doesn't translate well to a one-off CLI command; see the SDK tabs # for the full pause-and-continue flow. Single paused request: ant beta:messages create \ --beta compact-2026-01-12 \ --format jsonl <<'YAML' model: claude-opus-5 max_tokens: 4096 messages: - role: user content: Hello, Claude context_management: edits: - type: compact_20260112 pause_after_compaction: true YAML ``` ```python Python client = anthropic.Anthropic() messages = [{"role": "user", "content": "Hello, Claude"}] response = client.beta.messages.create( betas=["compact-2026-01-12"], model="claude-opus-5", max_tokens=4096, messages=messages, context_management={ "edits": [{"type": "compact_20260112", "pause_after_compaction": True}] }, ) # Check if compaction triggered a pause if response.stop_reason == "compaction": # Response contains only the compaction block messages.append({"role": "assistant", "content": response.content}) # Continue the request response = client.beta.messages.create( betas=["compact-2026-01-12"], model="claude-opus-5", max_tokens=4096, messages=messages, context_management={"edits": [{"type": "compact_20260112"}]}, ) ``` ```typescript TypeScript const client = new Anthropic(); const messages: Anthropic.Beta.Messages.BetaMessageParam[] = [ { role: "user", content: "Hello, Claude" } ]; let response = await client.beta.messages.create({ betas: ["compact-2026-01-12"], model: "claude-opus-5", max_tokens: 4096, messages, context_management: { edits: [ { type: "compact_20260112", pause_after_compaction: true } ] } }); // Check if compaction triggered a pause if (response.stop_reason === "compaction") { // Response contains only the compaction block messages.push({ role: "assistant", content: response.content }); // Continue the request response = await client.beta.messages.create({ betas: ["compact-2026-01-12"], model: "claude-opus-5", max_tokens: 4096, messages, context_management: { edits: [{ type: "compact_20260112" }] } }); } ``` ```csharp C# var client = new AnthropicClient(); var messages = new List { new() { Role = Role.User, Content = "Hello, Claude" } }; var parameters = new MessageCreateParams { Model = "claude-opus-5", MaxTokens = 4096, Betas = ["compact-2026-01-12"], Messages = messages, ContextManagement = new BetaContextManagementConfig { Edits = [new BetaCompact20260112Edit { PauseAfterCompaction = true }] } }; var response = await client.Beta.Messages.Create(parameters); if (response.StopReason == BetaStopReason.Compaction) { messages.Add(new BetaMessageParam { Role = Role.Assistant, Content = response.Content.Select(block => new BetaContentBlockParam(block.Json)).ToList() }); parameters = new() { Model = "claude-opus-5", MaxTokens = 4096, Betas = ["compact-2026-01-12"], Messages = messages, ContextManagement = new BetaContextManagementConfig { Edits = [new BetaCompact20260112Edit()] } }; response = await client.Beta.Messages.Create(parameters); } Console.WriteLine(response); ``` ```go Go client := anthropic.NewClient() messages := []anthropic.BetaMessageParam{anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Hello, Claude"))} compactEdit := anthropic.BetaContextManagementConfigParam{ Edits: []anthropic.BetaContextManagementConfigEditUnionParam{ {OfCompact20260112: &anthropic.BetaCompact20260112EditParam{ PauseAfterCompaction: anthropic.Bool(true), }}, }, } response, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 4096, Messages: messages, ContextManagement: compactEdit, Betas: []anthropic.AnthropicBeta{"compact-2026-01-12"}, }) if err != nil { log.Fatal(err) } if response.StopReason == "compaction" { messages = append(messages, response.ToParam()) response, err = client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 4096, Messages: messages, ContextManagement: anthropic.BetaContextManagementConfigParam{ Edits: []anthropic.BetaContextManagementConfigEditUnionParam{ {OfCompact20260112: &anthropic.BetaCompact20260112EditParam{}}, }, }, Betas: []anthropic.AnthropicBeta{"compact-2026-01-12"}, }) if err != nil { log.Fatal(err) } } fmt.Println(response) ``` ```java Java import com.anthropic.models.beta.messages.BetaContextManagementConfig; import com.anthropic.models.beta.messages.BetaCompact20260112Edit; import com.anthropic.models.beta.messages.BetaStopReason; // ... AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model("claude-opus-5") .maxTokens(4096L) .addBeta("compact-2026-01-12") .addUserMessage("Help me build a website") .contextManagement(BetaContextManagementConfig.builder() .addEdit(BetaCompact20260112Edit.builder() .pauseAfterCompaction(true) .build()) .build()) .build(); BetaMessage response = client.beta().messages().create(params); // Check if compaction triggered a pause if (response.stopReason().isPresent() && response.stopReason().get().equals(BetaStopReason.COMPACTION)) { // Append the compaction block and continue the request // by building a new request with the compacted context MessageCreateParams continueParams = MessageCreateParams.builder() .model("claude-opus-5") .maxTokens(4096L) .addBeta("compact-2026-01-12") .addUserMessage("Help me build a website") .addMessage(response) .contextManagement(BetaContextManagementConfig.builder() .addEdit(BetaCompact20260112Edit.builder().build()) .build()) .build(); response = client.beta().messages().create(continueParams); } System.out.println(response); ``` ```php PHP $client = new Client(); $messages = [['role' => 'user', 'content' => 'Hello, Claude']]; $response = $client->beta->messages->create( maxTokens: 4096, messages: $messages, model: 'claude-opus-5', betas: ['compact-2026-01-12'], contextManagement: [ 'edits' => [ [ 'type' => 'compact_20260112', 'pause_after_compaction' => true ] ] ] ); if ($response->stopReason === 'compaction') { $messages[] = [ 'role' => 'assistant', 'content' => $response->content ]; $response = $client->beta->messages->create( maxTokens: 4096, messages: $messages, model: 'claude-opus-5', betas: ['compact-2026-01-12'], contextManagement: [ 'edits' => [ ['type' => 'compact_20260112'] ] ] ); } echo $response; ``` ```ruby Ruby client = Anthropic::Client.new messages = [{ role: "user", content: "Hello, Claude" }] response = client.beta.messages.create( betas: ["compact-2026-01-12"], model: "claude-opus-5", max_tokens: 4096, messages: messages, context_management: { edits: [ { type: "compact_20260112", pause_after_compaction: true } ] } ) if response.stop_reason == :compaction messages << { role: "assistant", content: response.content } response = client.beta.messages.create( betas: ["compact-2026-01-12"], model: "claude-opus-5", max_tokens: 4096, messages: messages, context_management: { edits: [{ type: "compact_20260112" }] } ) end puts response ``` #### Enforcing a total token budget When a model works on long tasks with many tool-use iterations, total token consumption can grow significantly. You can combine `pause_after_compaction` with a compaction counter to estimate cumulative usage and gracefully wrap up the task once a budget is reached. This example appears in the SDK languages only: its value is the budget-tracking logic around the request. The raw request combines the `trigger` from [Trigger configuration](https://platform.claude.com/docs/en/build-with-claude/compaction#trigger-configuration) with `pause_after_compaction` from [Pausing after compaction](https://platform.claude.com/docs/en/build-with-claude/compaction#pausing-after-compaction). ```python Python client = anthropic.Anthropic() messages = [{"role": "user", "content": "Hello, Claude"}] TRIGGER_THRESHOLD = 100_000 TOTAL_TOKEN_BUDGET = 3_000_000 n_compactions = 0 response = client.beta.messages.create( betas=["compact-2026-01-12"], model="claude-opus-5", max_tokens=4096, messages=messages, context_management={ "edits": [ { "type": "compact_20260112", "trigger": {"type": "input_tokens", "value": TRIGGER_THRESHOLD}, "pause_after_compaction": True, } ] }, ) if response.stop_reason == "compaction": n_compactions += 1 messages.append({"role": "assistant", "content": response.content}) # Estimate total tokens consumed; prompt wrap-up if over budget if n_compactions * TRIGGER_THRESHOLD >= TOTAL_TOKEN_BUDGET: messages.append( { "role": "user", "content": "Please wrap up your current work and summarize the final state.", } ) ``` ```typescript TypeScript const client = new Anthropic(); const messages: Anthropic.Beta.Messages.BetaMessageParam[] = [ { role: "user", content: "Hello, Claude" } ]; const TRIGGER_THRESHOLD = 100_000; const TOTAL_TOKEN_BUDGET = 3_000_000; let compactionCount = 0; const response = await client.beta.messages.create({ betas: ["compact-2026-01-12"], model: "claude-opus-5", max_tokens: 4096, messages, context_management: { edits: [ { type: "compact_20260112", trigger: { type: "input_tokens", value: TRIGGER_THRESHOLD }, pause_after_compaction: true } ] } }); if (response.stop_reason === "compaction") { compactionCount += 1; messages.push({ role: "assistant", content: response.content }); // Estimate total tokens consumed; prompt wrap-up if over budget if (compactionCount * TRIGGER_THRESHOLD >= TOTAL_TOKEN_BUDGET) { messages.push({ role: "user", content: "Please wrap up your current work and summarize the final state." }); } } ``` ```csharp C# AnthropicClient client = new(); List messages = [new() { Role = Role.User, Content = "Hello, Claude" }]; const int TriggerThreshold = 100_000; const int TotalTokenBudget = 3_000_000; int compactionCount = 0; var response = await client.Beta.Messages.Create(new() { Betas = ["compact-2026-01-12"], Model = "claude-opus-5", MaxTokens = 4096, Messages = messages, ContextManagement = new BetaContextManagementConfig { Edits = [new BetaCompact20260112Edit { Trigger = new BetaInputTokensTrigger(TriggerThreshold), PauseAfterCompaction = true }] } }); if (response.StopReason == BetaStopReason.Compaction) { compactionCount += 1; messages.Add(new() { Role = Role.Assistant, Content = response.Content.Select(b => new BetaContentBlockParam(b.Json)).ToList() }); // Estimate total tokens consumed; prompt wrap-up if over budget if (compactionCount * TriggerThreshold >= TotalTokenBudget) { messages.Add(new() { Role = Role.User, Content = "Please wrap up your current work and summarize the final state." }); } } Console.WriteLine(response); ``` ```go Go client := anthropic.NewClient() messages := []anthropic.BetaMessageParam{anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Hello, Claude"))} const triggerThreshold = 100_000 const totalTokenBudget = 3_000_000 compactionCount := 0 response, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 4096, Messages: messages, ContextManagement: anthropic.BetaContextManagementConfigParam{ Edits: []anthropic.BetaContextManagementConfigEditUnionParam{ {OfCompact20260112: &anthropic.BetaCompact20260112EditParam{ Trigger: anthropic.BetaInputTokensTriggerParam{Value: triggerThreshold}, PauseAfterCompaction: anthropic.Bool(true), }}, }, }, Betas: []anthropic.AnthropicBeta{"compact-2026-01-12"}, }) if err != nil { log.Fatal(err) } if response.StopReason == "compaction" { compactionCount++ messages = append(messages, response.ToParam()) // Estimate total tokens consumed; prompt wrap-up if over budget if compactionCount*triggerThreshold >= totalTokenBudget { messages = append(messages, anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Please wrap up your current work and summarize the final state."))) } } fmt.Println(response) ``` ```java Java import com.anthropic.models.beta.messages.BetaContextManagementConfig; import com.anthropic.models.beta.messages.BetaCompact20260112Edit; import com.anthropic.models.beta.messages.BetaInputTokensTrigger; import com.anthropic.models.beta.messages.BetaStopReason; // ... AnthropicClient client = AnthropicOkHttpClient.fromEnv(); long triggerThreshold = 100_000; long totalTokenBudget = 3_000_000; int compactionCount = 0; List messages = new ArrayList<>(); messages.add(BetaMessageParam.builder() .role(BetaMessageParam.Role.USER) .content("Hello, Claude") .build()); MessageCreateParams params = MessageCreateParams.builder() .addBeta("compact-2026-01-12") .model("claude-opus-5") .maxTokens(4096L) .messages(messages) .contextManagement(BetaContextManagementConfig.builder() .addEdit(BetaCompact20260112Edit.builder() .trigger(BetaInputTokensTrigger.builder() .value(triggerThreshold) .build()) .pauseAfterCompaction(true) .build()) .build()) .build(); BetaMessage response = client.beta().messages().create(params); if (response.stopReason().isPresent() && response.stopReason().get().equals(BetaStopReason.COMPACTION)) { compactionCount += 1; messages.add(response.toParam()); // Estimate total tokens consumed; prompt wrap-up if over budget if (compactionCount * triggerThreshold >= totalTokenBudget) { messages.add(BetaMessageParam.builder() .role(BetaMessageParam.Role.USER) .content("Please wrap up your current work and summarize the final state.") .build()); } } System.out.println(response); ``` ```php PHP $client = new Client(); $triggerThreshold = 100_000; $totalTokenBudget = 3_000_000; $compactionCount = 0; $messages = [['role' => 'user', 'content' => 'Hello, Claude']]; $response = $client->beta->messages->create( maxTokens: 4096, messages: $messages, model: 'claude-opus-5', betas: ['compact-2026-01-12'], contextManagement: [ 'edits' => [ [ 'type' => 'compact_20260112', 'trigger' => ['type' => 'input_tokens', 'value' => $triggerThreshold], 'pause_after_compaction' => true ] ] ] ); if ($response->stopReason === 'compaction') { $compactionCount += 1; $messages[] = ['role' => 'assistant', 'content' => $response->content]; // Estimate total tokens consumed; prompt wrap-up if over budget if ($compactionCount * $triggerThreshold >= $totalTokenBudget) { $messages[] = [ 'role' => 'user', 'content' => 'Please wrap up your current work and summarize the final state.' ]; } } ``` ```ruby Ruby client = Anthropic::Client.new messages = [{ role: "user", content: "Hello, Claude" }] TRIGGER_THRESHOLD = 100_000 TOTAL_TOKEN_BUDGET = 3_000_000 compaction_count = 0 response = client.beta.messages.create( betas: ["compact-2026-01-12"], model: "claude-opus-5", max_tokens: 4096, messages: messages, context_management: { edits: [ { type: "compact_20260112", trigger: { type: "input_tokens", value: TRIGGER_THRESHOLD }, pause_after_compaction: true } ] } ) if response.stop_reason == :compaction compaction_count += 1 messages << { role: "assistant", content: response.content } # Estimate total tokens consumed; prompt wrap-up if over budget if compaction_count * TRIGGER_THRESHOLD >= TOTAL_TOKEN_BUDGET messages << { role: "user", content: "Please wrap up your current work and summarize the final state." } end end ``` ## Working with compaction blocks When compaction is triggered, the API returns a `compaction` block at the start of the assistant response. A long-running conversation might result in multiple compactions. The last compaction block reflects the final state of the prompt, replacing content prior to it with the generated summary. ```json Output { "content": [ { "type": "compaction", "content": "Summary of the conversation: The user requested help building a web scraper..." }, { "type": "text", "text": "Based on our conversation so far..." } ] } ``` ### Passing compaction blocks back You must pass the `compaction` block back to the API on subsequent requests to continue the conversation with the shortened prompt. The simplest approach is to append the entire response content to your messages: ```bash cURL # The response content, including the compaction block, must go back to the # API as the assistant turn of the next request. Managing that message list # doesn't translate well to a one-off shell command; see the CLI and SDK # tabs for the full flow. First request: curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: compact-2026-01-12" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 4096, "messages": [ { "role": "user", "content": "Hello, Claude" } ], "context_management": { "edits": [ { "type": "compact_20260112" } ] } }' ``` ```bash CLI ant beta:messages create \ --beta compact-2026-01-12 \ --transform content \ --format jsonl <<'YAML' > content.json model: claude-opus-5 max_tokens: 4096 messages: - role: user content: Hello, Claude context_management: edits: - type: compact_20260112 YAML # After receiving a response with a compaction block, append it as the # assistant turn and continue the conversation ant beta:messages create --beta compact-2026-01-12 < { new() { Role = Role.User, Content = "Help me build a web scraper" } }; var response = await client.Beta.Messages.Create(new() { Betas = ["compact-2026-01-12"], Model = "claude-opus-5", MaxTokens = 4096, Messages = messages, ContextManagement = new BetaContextManagementConfig { Edits = [new BetaCompact20260112Edit()] } }); messages.Add(new BetaMessageParam { Role = Role.Assistant, Content = response.Content.Select(block => new BetaContentBlockParam(block.Json)).ToList() }); messages.Add(new BetaMessageParam { Role = Role.User, Content = "Now add error handling" }); var nextResponse = await client.Beta.Messages.Create(new() { Betas = ["compact-2026-01-12"], Model = "claude-opus-5", MaxTokens = 4096, Messages = messages, ContextManagement = new BetaContextManagementConfig { Edits = [new BetaCompact20260112Edit()] } }); Console.WriteLine(nextResponse); ``` ```go Go client := anthropic.NewClient() messages := []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Help me build a web scraper")), } compactEdit := anthropic.BetaContextManagementConfigParam{ Edits: []anthropic.BetaContextManagementConfigEditUnionParam{ {OfCompact20260112: &anthropic.BetaCompact20260112EditParam{}}, }, } response, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 4096, Messages: messages, ContextManagement: compactEdit, Betas: []anthropic.AnthropicBeta{"compact-2026-01-12"}, }) if err != nil { log.Fatal(err) } messages = append(messages, response.ToParam()) messages = append(messages, anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Now add error handling"))) nextResponse, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 4096, Messages: messages, ContextManagement: compactEdit, Betas: []anthropic.AnthropicBeta{"compact-2026-01-12"}, }) if err != nil { log.Fatal(err) } fmt.Println(nextResponse) ``` ```java Java import com.anthropic.models.beta.messages.BetaContextManagementConfig; import com.anthropic.models.beta.messages.BetaCompact20260112Edit; // ... AnthropicClient client = AnthropicOkHttpClient.fromEnv(); // First request BetaMessage response = client.beta().messages().create( MessageCreateParams.builder() .addBeta("compact-2026-01-12") .model("claude-opus-5") .maxTokens(4096L) .addUserMessage("Help me build a web scraper") .contextManagement(BetaContextManagementConfig.builder() .addEdit(BetaCompact20260112Edit.builder().build()) .build()) .build()); // After receiving a response with a compaction block, append the full // content (including compaction blocks) and continue the conversation BetaMessage nextResponse = client.beta().messages().create( MessageCreateParams.builder() .addBeta("compact-2026-01-12") .model("claude-opus-5") .maxTokens(4096L) .addUserMessage("Help me build a web scraper") .addMessage(response) .addUserMessage("Now add error handling") .contextManagement(BetaContextManagementConfig.builder() .addEdit(BetaCompact20260112Edit.builder().build()) .build()) .build()); System.out.println(nextResponse); ``` ```php PHP $client = new Client(); $messages = [ ['role' => 'user', 'content' => 'Help me build a web scraper'] ]; $response = $client->beta->messages->create( maxTokens: 4096, messages: $messages, model: 'claude-opus-5', betas: ['compact-2026-01-12'], contextManagement: [ 'edits' => [['type' => 'compact_20260112']] ] ); $messages[] = ['role' => 'assistant', 'content' => $response->content]; $messages[] = ['role' => 'user', 'content' => 'Now add error handling']; $nextResponse = $client->beta->messages->create( maxTokens: 4096, messages: $messages, model: 'claude-opus-5', betas: ['compact-2026-01-12'], contextManagement: [ 'edits' => [['type' => 'compact_20260112']] ] ); echo json_encode($nextResponse, JSON_PRETTY_PRINT), PHP_EOL; ``` ```ruby Ruby client = Anthropic::Client.new messages = [ { role: "user", content: "Help me build a web scraper" } ] response = client.beta.messages.create( betas: ["compact-2026-01-12"], model: "claude-opus-5", max_tokens: 4096, messages: messages, context_management: { edits: [{ type: "compact_20260112" }] } ) messages << { role: "assistant", content: response.content } messages << { role: "user", content: "Now add error handling" } next_response = client.beta.messages.create( betas: ["compact-2026-01-12"], model: "claude-opus-5", max_tokens: 4096, messages: messages, context_management: { edits: [{ type: "compact_20260112" }] } ) puts next_response.content ``` When the API receives a `compaction` block, all content blocks before it are ignored. You can either: * Keep the original messages in your list and let the API handle removing the compacted content * Manually drop the compacted messages and only include the compaction block onwards ### Streaming The compaction block streams differently from text blocks. You receive a `content_block_start` event, followed by a single `content_block_delta` with the complete summary content (no intermediate streaming), and then a `content_block_stop` event. ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: compact-2026-01-12" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 4096, "stream": true, "messages": [ { "role": "user", "content": "Hello, Claude" } ], "context_management": { "edits": [ { "type": "compact_20260112" } ] } }' ``` ```bash CLI ant beta:messages create \ --stream \ --beta compact-2026-01-12 \ --format jsonl <<'YAML' model: claude-opus-5 max_tokens: 4096 messages: - role: user content: Hello, Claude context_management: edits: - type: compact_20260112 YAML ``` ```python Python client = anthropic.Anthropic() messages = [{"role": "user", "content": "Hello, Claude"}] with client.beta.messages.stream( betas=["compact-2026-01-12"], model="claude-opus-5", max_tokens=4096, messages=messages, context_management={"edits": [{"type": "compact_20260112"}]}, ) as stream: for event in stream: if event.type == "content_block_start": if event.content_block.type == "compaction": print("Compaction started...") elif event.content_block.type == "text": print("Text response started...") elif event.type == "content_block_delta": if event.delta.type == "compaction_delta": print(f"Compaction complete: {len(event.delta.content or '')} chars") elif event.delta.type == "text_delta": print(event.delta.text, end="", flush=True) # Get the final accumulated message message = stream.get_final_message() messages.append({"role": "assistant", "content": message.content}) ``` ```typescript TypeScript const client = new Anthropic(); const messages: Anthropic.Beta.Messages.BetaMessageParam[] = [ { role: "user", content: "Hello, Claude" } ]; const stream = await client.beta.messages.stream({ betas: ["compact-2026-01-12"], model: "claude-opus-5", max_tokens: 4096, messages, context_management: { edits: [{ type: "compact_20260112" }] } }); for await (const event of stream) { if (event.type === "content_block_start") { if (event.content_block.type === "compaction") { console.log("Compaction started..."); } else if (event.content_block.type === "text") { console.log("Text response started..."); } } else if (event.type === "content_block_delta") { if (event.delta.type === "compaction_delta") { console.log(`Compaction complete: ${event.delta.content?.length ?? 0} chars`); } else if (event.delta.type === "text_delta") { process.stdout.write(event.delta.text); } } } // Get the final accumulated message const message = await stream.finalMessage(); messages.push({ role: "assistant", content: message.content }); ``` ```csharp C# var client = new AnthropicClient(); List messages = [new() { Role = Role.User, Content = "Hello" }]; var parameters = new MessageCreateParams { Betas = ["compact-2026-01-12"], Model = "claude-opus-5", MaxTokens = 4096, Messages = messages, ContextManagement = new BetaContextManagementConfig { Edits = [new BetaCompact20260112Edit()] } }; await foreach (var streamEvent in client.Beta.Messages.CreateStreaming(parameters)) { if (streamEvent.TryPickContentBlockStart(out var startEvent)) { if (startEvent.ContentBlock.TryPickBetaCompaction(out _)) { Console.WriteLine("Compaction started..."); } else if (startEvent.ContentBlock.TryPickBetaText(out _)) { Console.WriteLine("Text response started..."); } } else if (streamEvent.TryPickContentBlockDelta(out var deltaEvent)) { if (deltaEvent.Delta.TryPickCompaction(out var compactionDelta)) { Console.WriteLine($"Compaction complete: {compactionDelta.Content?.Length ?? 0} chars"); } else if (deltaEvent.Delta.TryPickText(out var textDelta)) { Console.Write(textDelta.Text); } } } ``` ```go Go client := anthropic.NewClient() messages := []anthropic.BetaMessageParam{anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Hello, Claude"))} stream := client.Beta.Messages.NewStreaming(context.TODO(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 4096, Messages: messages, ContextManagement: anthropic.BetaContextManagementConfigParam{ Edits: []anthropic.BetaContextManagementConfigEditUnionParam{ {OfCompact20260112: &anthropic.BetaCompact20260112EditParam{}}, }, }, Betas: []anthropic.AnthropicBeta{"compact-2026-01-12"}, }) for stream.Next() { event := stream.Current() switch eventVariant := event.AsAny().(type) { case anthropic.BetaRawContentBlockStartEvent: switch eventVariant.ContentBlock.AsAny().(type) { case anthropic.BetaCompactionBlock: fmt.Println("Compaction started...") case anthropic.BetaTextBlock: fmt.Println("Text response started...") } case anthropic.BetaRawContentBlockDeltaEvent: switch deltaVariant := eventVariant.Delta.AsAny().(type) { case anthropic.BetaCompactionContentBlockDelta: fmt.Printf("Compaction complete: %d chars\n", len(deltaVariant.Content)) case anthropic.BetaTextDelta: fmt.Print(deltaVariant.Text) } } } if err := stream.Err(); err != nil { log.Fatal(err) } ``` ```java Java import com.anthropic.models.beta.messages.BetaContextManagementConfig; import com.anthropic.models.beta.messages.BetaCompact20260112Edit; // ... AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model("claude-opus-5") .maxTokens(4096L) .addBeta("compact-2026-01-12") .addUserMessage("Hello, Claude") .contextManagement(BetaContextManagementConfig.builder() .addEdit(BetaCompact20260112Edit.builder().build()) .build()) .build(); try (var streamResponse = client.beta().messages().createStreaming(params)) { streamResponse.stream().forEach(event -> { event.contentBlockStart().ifPresent(startEvent -> { startEvent.contentBlock().compaction().ifPresent(c -> System.out.println("Compaction started...") ); startEvent.contentBlock().text().ifPresent(t -> System.out.println("Text response started...") ); }); event.contentBlockDelta().ifPresent(deltaEvent -> { deltaEvent.delta().compaction().ifPresent(cd -> System.out.println("Compaction complete: " + cd.content().map(String::length).orElse(0) + " chars") ); deltaEvent.delta().text().ifPresent(td -> System.out.print(td.text()) ); }); }); } ``` ```php PHP $client = new Client(); $messages = [['role' => 'user', 'content' => 'Hello, Claude']]; $stream = $client->beta->messages->createStream( maxTokens: 4096, messages: $messages, model: 'claude-opus-5', betas: ['compact-2026-01-12'], contextManagement: [ 'edits' => [ ['type' => 'compact_20260112'] ] ] ); foreach ($stream as $event) { if ($event->type === 'content_block_start') { if ($event->contentBlock->type === 'compaction') { echo "Compaction started...\n"; } elseif ($event->contentBlock->type === 'text') { echo "Text response started...\n"; } } elseif ($event->type === 'content_block_delta') { if ($event->delta->type === 'compaction_delta') { echo "Compaction complete: " . strlen($event->delta->content ?? '') . " chars\n"; } elseif ($event->delta->type === 'text_delta') { echo $event->delta->text; } } } ``` ```ruby Ruby client = Anthropic::Client.new messages = [{ role: "user", content: "Hello, Claude" }] stream = client.beta.messages.stream( betas: ["compact-2026-01-12"], model: "claude-opus-5", max_tokens: 4096, messages: messages, context_management: { edits: [{ type: "compact_20260112" }] } ) stream.each do |event| case event.type when :content_block_start if event.content_block.type == :compaction puts "Compaction started..." elsif event.content_block.type == :text puts "Text response started..." end when :content_block_delta if event.delta.type == :compaction_delta puts "Compaction complete: #{(event.delta.content || "").length} chars" elsif event.delta.type == :text_delta print event.delta.text end end end ``` ### Prompt caching Compaction works well with [prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching). You can add a `cache_control` breakpoint on compaction blocks to cache the summarized content. ```json { "role": "assistant", "content": [ { "type": "compaction", "content": "[summary text]", "cache_control": { "type": "ephemeral" } }, { "type": "text", "text": "Based on our conversation..." } ] } ``` #### Maximizing cache hits with system prompts When compaction occurs, the summary becomes new content that needs to be written to the cache. Without additional cache breakpoints, this would also invalidate any cached system prompt, requiring it to be re-cached along with the compaction summary. To maximize cache hit rates, add a `cache_control` breakpoint at the end of your system prompt. This keeps the system prompt cached separately from the conversation, so when compaction occurs: * The system prompt cache remains valid and is read from cache * Only the compaction summary needs to be written as a new cache entry ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: compact-2026-01-12" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 4096, "system": [ { "type": "text", "text": "You are a helpful coding assistant...", "cache_control": { "type": "ephemeral" } } ], "messages": [ { "role": "user", "content": "Hello, Claude" } ], "context_management": { "edits": [ { "type": "compact_20260112" } ] } }' ``` ```bash CLI ant beta:messages create --beta compact-2026-01-12 <<'YAML' model: claude-opus-5 max_tokens: 4096 system: - type: text text: You are a helpful coding assistant... cache_control: type: ephemeral messages: - role: user content: Hello, Claude context_management: edits: - type: compact_20260112 YAML ``` ```python Python client = anthropic.Anthropic() messages = [{"role": "user", "content": "Hello, Claude"}] response = client.beta.messages.create( betas=["compact-2026-01-12"], model="claude-opus-5", max_tokens=4096, system=[ { "type": "text", "text": "You are a helpful coding assistant...", "cache_control": { "type": "ephemeral" }, # Cache the system prompt separately } ], messages=messages, context_management={"edits": [{"type": "compact_20260112"}]}, ) ``` ```typescript TypeScript const client = new Anthropic(); const messages: Anthropic.Beta.Messages.BetaMessageParam[] = [ { role: "user", content: "Hello, Claude" } ]; const response = await client.beta.messages.create({ betas: ["compact-2026-01-12"], model: "claude-opus-5", max_tokens: 4096, system: [ { type: "text", text: "You are a helpful coding assistant...", cache_control: { type: "ephemeral" } // Cache the system prompt separately } ], messages, context_management: { edits: [{ type: "compact_20260112" }] } }); ``` ```csharp C# var client = new AnthropicClient(); var parameters = new MessageCreateParams { Betas = ["compact-2026-01-12"], Model = "claude-opus-5", MaxTokens = 4096, System = new List { new() { Text = "You are a helpful coding assistant...", CacheControl = new BetaCacheControlEphemeral() } }, Messages = [new() { Role = Role.User, Content = "Hello, Claude" }], ContextManagement = new BetaContextManagementConfig { Edits = [new BetaCompact20260112Edit()] } }; var response = await client.Beta.Messages.Create(parameters); Console.WriteLine(response); ``` ```go Go client := anthropic.NewClient() response, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 4096, System: []anthropic.BetaTextBlockParam{ { Text: "You are a helpful coding assistant...", CacheControl: anthropic.NewBetaCacheControlEphemeralParam(), }, }, Messages: []anthropic.BetaMessageParam{anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Hello, Claude"))}, ContextManagement: anthropic.BetaContextManagementConfigParam{ Edits: []anthropic.BetaContextManagementConfigEditUnionParam{ {OfCompact20260112: &anthropic.BetaCompact20260112EditParam{}}, }, }, Betas: []anthropic.AnthropicBeta{"compact-2026-01-12"}, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java import com.anthropic.models.beta.messages.BetaContextManagementConfig; import com.anthropic.models.beta.messages.BetaCompact20260112Edit; import com.anthropic.models.beta.messages.BetaCacheControlEphemeral; // ... AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model("claude-opus-5") .maxTokens(4096L) .addBeta("compact-2026-01-12") .systemOfBetaTextBlockParams(List.of( BetaTextBlockParam.builder() .text("You are a helpful coding assistant...") .cacheControl(BetaCacheControlEphemeral.builder().build()) .build() )) .addUserMessage("Hello, Claude") .contextManagement(BetaContextManagementConfig.builder() .addEdit(BetaCompact20260112Edit.builder().build()) .build()) .build(); BetaMessage response = client.beta().messages().create(params); System.out.println(response); ``` ```php PHP $client = new Client(); $response = $client->beta->messages->create( maxTokens: 4096, messages: [['role' => 'user', 'content' => 'Hello, Claude']], model: 'claude-opus-5', betas: ['compact-2026-01-12'], system: [ [ 'type' => 'text', 'text' => 'You are a helpful coding assistant...', 'cache_control' => [ 'type' => 'ephemeral' ] ] ], contextManagement: [ 'edits' => [ ['type' => 'compact_20260112'] ] ] ); echo json_encode($response, JSON_PRETTY_PRINT), PHP_EOL; ``` ```ruby Ruby client = Anthropic::Client.new response = client.beta.messages.create( betas: ["compact-2026-01-12"], model: "claude-opus-5", max_tokens: 4096, system: [ { type: "text", text: "You are a helpful coding assistant...", cache_control: { type: "ephemeral" } } ], messages: [{ role: "user", content: "Hello, Claude" }], context_management: { edits: [{ type: "compact_20260112" }] } ) puts response ``` This keeps long system prompts cached across multiple compaction events throughout a conversation. ## Understanding usage Compaction requires an additional sampling step, which contributes to rate limits and billing. The API returns detailed usage information in the response: ```json Output { "usage": { "input_tokens": 23000, "output_tokens": 1000, "iterations": [ { "type": "compaction", "input_tokens": 180000, "output_tokens": 3500 }, { "type": "message", "input_tokens": 23000, "output_tokens": 1000 } ] } } ``` The `iterations` array shows usage for each sampling iteration. When compaction occurs, you'll see a `compaction` iteration followed by the main `message` iteration. The top-level `input_tokens` and `output_tokens` match the `message` iteration exactly in this example because there is only one non-compaction iteration. The final iteration's token counts reflect the effective context size after compaction. The top-level `input_tokens` and `output_tokens` do not include compaction iteration usage. They reflect the sum of all non-compaction iterations. To calculate total tokens consumed and billed for a request, sum across all entries in the `usage.iterations` array. If you previously relied on `usage.input_tokens` and `usage.output_tokens` for cost tracking or auditing, you'll need to update your tracking logic to aggregate across `usage.iterations` when compaction is enabled. With the compaction beta enabled, every response includes `usage.iterations`, even if no compaction occurred. A `compaction` entry appears only when a new compaction is triggered during the request. Re-applying a previous `compaction` block incurs no additional compaction cost, and the top-level usage fields remain accurate in that case. ## Combining with other features ### Server tools When using server tools (such as web search), the compaction trigger is checked at the start of each sampling iteration. Compaction might occur multiple times within a single request depending on your trigger threshold and the amount of output generated. ### Token counting The token counting endpoint (`/v1/messages/count_tokens`) applies existing `compaction` blocks in your prompt but does not trigger new compactions. Use it to check your effective token count after previous compactions: ```bash cURL curl https://api.anthropic.com/v1/messages/count_tokens \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: compact-2026-01-12" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "messages": [ { "role": "user", "content": "Hello, Claude" } ], "context_management": { "edits": [ { "type": "compact_20260112" } ] } }' ``` ```bash CLI cat > request.yaml <<'YAML' model: claude-opus-5 messages: - role: user content: Hello, Claude context_management: edits: - type: compact_20260112 YAML CURRENT=$(ant beta:messages count-tokens \ --beta compact-2026-01-12 \ --transform input_tokens \ --raw-output < request.yaml) ORIGINAL=$(ant beta:messages count-tokens \ --beta compact-2026-01-12 \ --transform context_management.original_input_tokens \ --raw-output < request.yaml) printf 'Current tokens: %s\n' "$CURRENT" printf 'Original tokens: %s\n' "$ORIGINAL" ``` ```python Python client = anthropic.Anthropic() messages = [{"role": "user", "content": "Hello, Claude"}] count_response = client.beta.messages.count_tokens( betas=["compact-2026-01-12"], model="claude-opus-5", messages=messages, context_management={"edits": [{"type": "compact_20260112"}]}, ) print(f"Current tokens: {count_response.input_tokens}") print(f"Original tokens: {count_response.context_management.original_input_tokens}") ``` ```typescript TypeScript const client = new Anthropic(); const messages: Anthropic.Beta.Messages.BetaMessageParam[] = [ { role: "user", content: "Summarize the key points of our conversation so far." } ]; const countResponse = await client.beta.messages.countTokens({ betas: ["compact-2026-01-12"], model: "claude-opus-5", messages, context_management: { edits: [{ type: "compact_20260112" }] } }); console.log(`Current tokens: ${countResponse.input_tokens}`); console.log(`Original tokens: ${countResponse.context_management!.original_input_tokens}`); ``` ```csharp C# AnthropicClient client = new(); List messages = [new() { Role = Role.User, Content = "Hello" }]; var countParams = new MessageCountTokensParams { Model = "claude-opus-5", Messages = messages, ContextManagement = new BetaContextManagementConfig { Edits = [new BetaCompact20260112Edit()] }, Betas = ["compact-2026-01-12"] }; var countResponse = await client.Beta.Messages.CountTokens(countParams); Console.WriteLine($"Current tokens: {countResponse.InputTokens}"); Console.WriteLine($"Original tokens: {countResponse.ContextManagement?.OriginalInputTokens}"); ``` ```go Go client := anthropic.NewClient() messages := []anthropic.BetaMessageParam{anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Hello, Claude"))} countResponse, err := client.Beta.Messages.CountTokens(context.TODO(), anthropic.BetaMessageCountTokensParams{ Model: anthropic.ModelClaudeOpus5, Messages: messages, ContextManagement: anthropic.BetaContextManagementConfigParam{ Edits: []anthropic.BetaContextManagementConfigEditUnionParam{ {OfCompact20260112: &anthropic.BetaCompact20260112EditParam{}}, }, }, Betas: []anthropic.AnthropicBeta{"compact-2026-01-12"}, }) if err != nil { log.Fatal(err) } fmt.Printf("Current tokens: %d\n", countResponse.InputTokens) fmt.Printf("Original tokens: %d\n", countResponse.ContextManagement.OriginalInputTokens) ``` ```java Java import com.anthropic.models.beta.messages.BetaMessageTokensCount; import com.anthropic.models.beta.messages.MessageCountTokensParams; import com.anthropic.models.beta.messages.BetaContextManagementConfig; import com.anthropic.models.beta.messages.BetaCompact20260112Edit; // ... AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCountTokensParams params = MessageCountTokensParams.builder() .model("claude-opus-5") .addUserMessage("Hello, Claude") .contextManagement(BetaContextManagementConfig.builder() .addEdit(BetaCompact20260112Edit.builder().build()) .build()) .addBeta("compact-2026-01-12") .build(); BetaMessageTokensCount countResponse = client.beta().messages().countTokens(params); System.out.println("Current tokens: " + countResponse.inputTokens()); System.out.println("Original tokens: " + countResponse.contextManagement().get().originalInputTokens()); ``` ```php PHP $client = new Client(); $messages = [['role' => 'user', 'content' => 'Hello, Claude']]; $countResponse = $client->beta->messages->countTokens( messages: $messages, model: 'claude-opus-5', betas: ['compact-2026-01-12'], contextManagement: [ 'edits' => [ ['type' => 'compact_20260112'] ] ] ); echo "Current tokens: " . $countResponse->inputTokens . "\n"; echo "Original tokens: " . $countResponse->contextManagement->originalInputTokens . "\n"; ``` ```ruby Ruby client = Anthropic::Client.new messages = [{ role: "user", content: "Hello, Claude" }] count_response = client.beta.messages.count_tokens( betas: ["compact-2026-01-12"], model: "claude-opus-5", messages: messages, context_management: { edits: [{ type: "compact_20260112" }] } ) puts "Current tokens: #{count_response.input_tokens}" puts "Original tokens: #{count_response.context_management.original_input_tokens}" ``` ## Examples Here's a complete example of a long-running conversation with compaction: ```bash cURL # curl sends individual requests; maintain the messages array in the # calling script. See the SDK tabs for the full chat() loop. Single-turn # request shape: curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: compact-2026-01-12" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 4096, "messages": [ { "role": "user", "content": "Help me build a Python web scraper" } ], "context_management": { "edits": [ { "type": "compact_20260112", "trigger": { "type": "input_tokens", "value": 100000 } } ] } }' ``` ```bash CLI # The CLI handles individual turns; maintain the messages array in the # calling script. See the SDK tabs for the full chat() loop. Single-turn # request shape: ant beta:messages create \ --beta compact-2026-01-12 \ --transform 'content.#(type=="text").text' \ --raw-output <<'YAML' model: claude-opus-5 max_tokens: 4096 messages: - role: user content: Help me build a Python web scraper context_management: edits: - type: compact_20260112 trigger: type: input_tokens value: 100000 YAML ``` ```python Python client = anthropic.Anthropic() messages: list[dict] = [] def chat(user_message: str) -> str: messages.append({"role": "user", "content": user_message}) response = client.beta.messages.create( betas=["compact-2026-01-12"], model="claude-opus-5", max_tokens=4096, messages=messages, context_management={ "edits": [ { "type": "compact_20260112", "trigger": {"type": "input_tokens", "value": 100000}, } ] }, ) # Append response (compaction blocks are automatically included) messages.append({"role": "assistant", "content": response.content}) # Return the text content return next(block.text for block in response.content if block.type == "text") # Run a long conversation print(chat("Help me build a Python web scraper")) print(chat("Add support for JavaScript-rendered pages")) print(chat("Now add rate limiting and error handling")) # Continue calling chat() for as long as the conversation needs ``` ```typescript TypeScript const client = new Anthropic(); const messages: Anthropic.Beta.Messages.BetaMessageParam[] = []; async function chat(userMessage: string): Promise { messages.push({ role: "user", content: userMessage }); const response = await client.beta.messages.create({ betas: ["compact-2026-01-12"], model: "claude-opus-5", max_tokens: 4096, messages, context_management: { edits: [ { type: "compact_20260112", trigger: { type: "input_tokens", value: 100000 } } ] } }); // Append response (compaction blocks are automatically included) messages.push({ role: "assistant", content: response.content }); // Return the text content const textBlock = response.content.find((block) => block.type === "text"); return textBlock?.text ?? ""; } // Run a long conversation console.log(await chat("Help me build a Python web scraper")); console.log(await chat("Add support for JavaScript-rendered pages")); console.log(await chat("Now add rate limiting and error handling")); // Continue calling chat() for as long as the conversation needs ``` ```csharp C# AnthropicClient client = new(); List messages = new(); Console.WriteLine(await Chat(client, messages, "Help me build a Python web scraper")); Console.WriteLine(await Chat(client, messages, "Add support for JavaScript-rendered pages")); Console.WriteLine(await Chat(client, messages, "Now add rate limiting and error handling")); static async Task Chat(AnthropicClient client, List messages, string userMessage) { messages.Add(new() { Role = Role.User, Content = userMessage }); var parameters = new MessageCreateParams { Betas = ["compact-2026-01-12"], Model = "claude-opus-5", MaxTokens = 4096, Messages = messages, ContextManagement = new BetaContextManagementConfig { Edits = [new BetaCompact20260112Edit { Trigger = new BetaInputTokensTrigger(100000) }] } }; var response = await client.Beta.Messages.Create(parameters); messages.Add(new() { Role = Role.Assistant, Content = response.Content.Select(block => new BetaContentBlockParam(block.Json)).ToList() }); return response.Content .Select(block => block.Value) .OfType() .Select(tb => tb.Text) .FirstOrDefault() ?? ""; } ``` ```go Go package main import ( "context" "fmt" "log" "github.com/anthropics/anthropic-sdk-go" ) var ( client = anthropic.NewClient() messages []anthropic.BetaMessageParam ) func chat(userMessage string) string { messages = append(messages, anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock(userMessage))) response, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 4096, Messages: messages, ContextManagement: anthropic.BetaContextManagementConfigParam{ Edits: []anthropic.BetaContextManagementConfigEditUnionParam{ {OfCompact20260112: &anthropic.BetaCompact20260112EditParam{ Trigger: anthropic.BetaInputTokensTriggerParam{Value: 100000}, }}, }, }, Betas: []anthropic.AnthropicBeta{"compact-2026-01-12"}, }) if err != nil { log.Fatal(err) } messages = append(messages, response.ToParam()) for _, block := range response.Content { if variant, ok := block.AsAny().(anthropic.BetaTextBlock); ok { return variant.Text } } return "" } func main() { fmt.Println(chat("Help me build a Python web scraper")) fmt.Println(chat("Add support for JavaScript-rendered pages")) fmt.Println(chat("Now add rate limiting and error handling")) } ``` ```java Java import com.anthropic.models.beta.messages.BetaContextManagementConfig; import com.anthropic.models.beta.messages.BetaCompact20260112Edit; import com.anthropic.models.beta.messages.BetaInputTokensTrigger; // ... private static final AnthropicClient client = AnthropicOkHttpClient.fromEnv(); private static final List messages = new ArrayList<>(); public static void main(String[] args) { System.out.println(chat("Help me build a Python web scraper")); System.out.println(chat("Add support for JavaScript-rendered pages")); System.out.println(chat("Now add rate limiting and error handling")); } private static String chat(String userMessage) { messages.add(BetaMessageParam.builder() .role(BetaMessageParam.Role.USER) .content(userMessage) .build()); MessageCreateParams params = MessageCreateParams.builder() .addBeta("compact-2026-01-12") .model("claude-opus-5") .maxTokens(4096L) .messages(messages) .contextManagement(BetaContextManagementConfig.builder() .addEdit(BetaCompact20260112Edit.builder() .trigger(BetaInputTokensTrigger.builder() .value(100000L) .build()) .build()) .build()) .build(); BetaMessage response = client.beta().messages().create(params); // Append response (compaction blocks are automatically included) messages.add(response.toParam()); return response.content().stream() .filter(block -> block.text().isPresent()) .map(block -> block.text().get().text()) .findFirst() .orElse(""); } ``` ```php PHP $client = new Client(); $messages = []; function chat($client, &$messages, $userMessage) { $messages[] = ['role' => 'user', 'content' => $userMessage]; $response = $client->beta->messages->create( maxTokens: 4096, messages: $messages, model: 'claude-opus-5', betas: ['compact-2026-01-12'], contextManagement: [ 'edits' => [ [ 'type' => 'compact_20260112', 'trigger' => ['type' => 'input_tokens', 'value' => 100000] ] ] ] ); $messages[] = ['role' => 'assistant', 'content' => $response->content]; foreach ($response->content as $block) { if ($block->type === 'text') { return $block->text; } } return ''; } echo chat($client, $messages, "Help me build a Python web scraper") . "\n"; echo chat($client, $messages, "Add support for JavaScript-rendered pages") . "\n"; echo chat($client, $messages, "Now add rate limiting and error handling") . "\n"; ``` ```ruby Ruby client = Anthropic::Client.new messages = [] def chat(client, messages, user_message) messages << { role: "user", content: user_message } response = client.beta.messages.create( betas: ["compact-2026-01-12"], model: "claude-opus-5", max_tokens: 4096, messages: messages, context_management: { edits: [ { type: "compact_20260112", trigger: { type: "input_tokens", value: 100000 } } ] } ) messages << { role: "assistant", content: response.content } response.content.find { |block| block.type == :text }&.text || "" end puts chat(client, messages, "Help me build a Python web scraper") puts chat(client, messages, "Add support for JavaScript-rendered pages") puts chat(client, messages, "Now add rate limiting and error handling") ``` Here's an example that uses `pause_after_compaction` to preserve the prior exchange and the current user message (three messages total) verbatim instead of summarizing them: ```bash cURL # curl sends individual requests; maintain the messages array in the # calling script. See the SDK tabs for the full chat() loop with # pause-and-preserve handling. Single-turn request shape: curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: compact-2026-01-12" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 4096, "messages": [ { "role": "user", "content": "Help me build a Python web scraper" } ], "context_management": { "edits": [ { "type": "compact_20260112", "trigger": { "type": "input_tokens", "value": 100000 }, "pause_after_compaction": true } ] } }' ``` ```bash CLI # The CLI handles individual turns; maintain the messages array in the # calling script. See the SDK tabs for the full chat() loop with # pause-and-preserve handling. Single-turn request shape: ant beta:messages create \ --beta compact-2026-01-12 \ --transform 'content.#(type=="text").text' \ --raw-output <<'YAML' model: claude-opus-5 max_tokens: 4096 messages: - role: user content: Help me build a Python web scraper context_management: edits: - type: compact_20260112 trigger: type: input_tokens value: 100000 pause_after_compaction: true YAML ``` ```python Python from typing import Any client = anthropic.Anthropic() messages: list[dict[str, Any]] = [] def chat(user_message: str) -> str: messages.append({"role": "user", "content": user_message}) response = client.beta.messages.create( betas=["compact-2026-01-12"], model="claude-opus-5", max_tokens=4096, messages=messages, context_management={ "edits": [ { "type": "compact_20260112", "trigger": {"type": "input_tokens", "value": 100000}, "pause_after_compaction": True, } ] }, ) # Check if compaction occurred and paused if response.stop_reason == "compaction": # Get the compaction block from the response compaction_block = response.content[0] # Preserve the prior exchange + current user message (3 messages) # by including them after the compaction block preserved_messages = messages[-3:] if len(messages) >= 3 else messages # Build new message list: compaction + preserved messages new_assistant_content = [compaction_block] messages_after_compaction = [ {"role": "assistant", "content": new_assistant_content} ] + preserved_messages # Continue the request with the compacted context + preserved messages response = client.beta.messages.create( betas=["compact-2026-01-12"], model="claude-opus-5", max_tokens=4096, messages=messages_after_compaction, context_management={"edits": [{"type": "compact_20260112"}]}, ) # Update the message list to reflect the compaction messages.clear() messages.extend(messages_after_compaction) # Append the final response messages.append({"role": "assistant", "content": response.content}) # Return the text content return next(block.text for block in response.content if block.type == "text") # Run a long conversation print(chat("Help me build a Python web scraper")) print(chat("Add support for JavaScript-rendered pages")) print(chat("Now add rate limiting and error handling")) # Continue calling chat() for as long as the conversation needs ``` ```typescript TypeScript const client = new Anthropic(); let messages: Anthropic.Beta.Messages.BetaMessageParam[] = []; async function chat(userMessage: string): Promise { messages.push({ role: "user", content: userMessage }); let response = await client.beta.messages.create({ betas: ["compact-2026-01-12"], model: "claude-opus-5", max_tokens: 4096, messages, context_management: { edits: [ { type: "compact_20260112", trigger: { type: "input_tokens", value: 100000 }, pause_after_compaction: true } ] } }); // Check if compaction occurred and paused if (response.stop_reason === "compaction") { // Get the compaction block from the response const compactionBlock = response.content[0]; // Preserve the prior exchange + current user message (3 messages) // by including them after the compaction block const preservedMessages = messages.length >= 3 ? messages.slice(-3) : [...messages]; // Build new message list: compaction + preserved messages const messagesAfterCompaction: Anthropic.Beta.Messages.BetaMessageParam[] = [ { role: "assistant", content: [compactionBlock] }, ...preservedMessages ]; // Continue the request with the compacted context + preserved messages response = await client.beta.messages.create({ betas: ["compact-2026-01-12"], model: "claude-opus-5", max_tokens: 4096, messages: messagesAfterCompaction, context_management: { edits: [{ type: "compact_20260112" }] } }); // Update the message list to reflect the compaction messages = messagesAfterCompaction; } // Append the final response messages.push({ role: "assistant", content: response.content }); // Return the text content const textBlock = response.content.find((block) => block.type === "text"); return textBlock?.text ?? ""; } // Run a long conversation console.log(await chat("Help me build a Python web scraper")); console.log(await chat("Add support for JavaScript-rendered pages")); console.log(await chat("Now add rate limiting and error handling")); // Continue calling chat() for as long as the conversation needs ``` ```csharp C# AnthropicClient client = new(); List messages = new(); Console.WriteLine(await Chat("Help me build a Python web scraper")); Console.WriteLine(await Chat("Add support for JavaScript-rendered pages")); Console.WriteLine(await Chat("Now add rate limiting and error handling")); async Task Chat(string userMessage) { messages.Add(new() { Role = Role.User, Content = userMessage }); var response = await client.Beta.Messages.Create(new() { Betas = ["compact-2026-01-12"], Model = "claude-opus-5", MaxTokens = 4096, Messages = messages, ContextManagement = new BetaContextManagementConfig { Edits = [new BetaCompact20260112Edit { Trigger = new BetaInputTokensTrigger(100000), PauseAfterCompaction = true }] } }); if (response.StopReason == BetaStopReason.Compaction) { if (!response.Content[0].TryPickCompaction(out _)) throw new InvalidOperationException("Expected compaction block"); var preserved = messages.Count >= 3 ? messages.Skip(messages.Count - 3).ToList() : new List(messages); var messagesAfterCompaction = new List { new() { Role = Role.Assistant, Content = new List { new BetaContentBlockParam(response.Content[0].Json) } } }; messagesAfterCompaction.AddRange(preserved); response = await client.Beta.Messages.Create(new() { Betas = ["compact-2026-01-12"], Model = "claude-opus-5", MaxTokens = 4096, Messages = messagesAfterCompaction, ContextManagement = new BetaContextManagementConfig { Edits = [new BetaCompact20260112Edit()] } }); messages = messagesAfterCompaction; } messages.Add(new() { Role = Role.Assistant, Content = response.Content.Select(block => new BetaContentBlockParam(block.Json)).ToList() }); return response.Content .Select(block => block.Value) .OfType() .Select(tb => tb.Text) .FirstOrDefault() ?? ""; } ``` ```go Go package main import ( "context" "fmt" "log" "github.com/anthropics/anthropic-sdk-go" ) var ( client = anthropic.NewClient() messages []anthropic.BetaMessageParam ) func chat(userMessage string) string { messages = append(messages, anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock(userMessage))) compactEdit := anthropic.BetaContextManagementConfigParam{ Edits: []anthropic.BetaContextManagementConfigEditUnionParam{ {OfCompact20260112: &anthropic.BetaCompact20260112EditParam{ Trigger: anthropic.BetaInputTokensTriggerParam{Value: 100000}, PauseAfterCompaction: anthropic.Bool(true), }}, }, } response, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 4096, Messages: messages, ContextManagement: compactEdit, Betas: []anthropic.AnthropicBeta{"compact-2026-01-12"}, }) if err != nil { log.Fatal(err) } if response.StopReason == "compaction" { compactionParam := response.Content[0].ToParam() var preserved []anthropic.BetaMessageParam if len(messages) >= 3 { preserved = messages[len(messages)-3:] } else { preserved = messages } messagesAfterCompaction := []anthropic.BetaMessageParam{ {Role: anthropic.BetaMessageParamRoleAssistant, Content: []anthropic.BetaContentBlockParamUnion{compactionParam}}, } messagesAfterCompaction = append(messagesAfterCompaction, preserved...) response, err = client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 4096, Messages: messagesAfterCompaction, ContextManagement: anthropic.BetaContextManagementConfigParam{ Edits: []anthropic.BetaContextManagementConfigEditUnionParam{ {OfCompact20260112: &anthropic.BetaCompact20260112EditParam{}}, }, }, Betas: []anthropic.AnthropicBeta{"compact-2026-01-12"}, }) if err != nil { log.Fatal(err) } messages = messagesAfterCompaction } messages = append(messages, response.ToParam()) for _, block := range response.Content { if textBlock, ok := block.AsAny().(anthropic.BetaTextBlock); ok { return textBlock.Text } } return "" } func main() { fmt.Println(chat("Help me build a Python web scraper")) fmt.Println(chat("Add support for JavaScript-rendered pages")) fmt.Println(chat("Now add rate limiting and error handling")) } ``` ```java Java import com.anthropic.models.beta.messages.BetaContextManagementConfig; import com.anthropic.models.beta.messages.BetaCompact20260112Edit; import com.anthropic.models.beta.messages.BetaInputTokensTrigger; import com.anthropic.models.beta.messages.BetaStopReason; // ... private static final AnthropicClient client = AnthropicOkHttpClient.fromEnv(); private static final List messages = new ArrayList<>(); public static String chat(String userMessage) { messages.add(BetaMessageParam.builder() .role(BetaMessageParam.Role.USER) .content(userMessage) .build()); MessageCreateParams params = MessageCreateParams.builder() .addBeta("compact-2026-01-12") .model("claude-opus-5") .maxTokens(4096L) .messages(messages) .contextManagement(BetaContextManagementConfig.builder() .addEdit(BetaCompact20260112Edit.builder() .trigger(BetaInputTokensTrigger.builder() .value(100000L) .build()) .pauseAfterCompaction(true) .build()) .build()) .build(); BetaMessage response = client.beta().messages().create(params); // Check if compaction occurred and paused if (response.stopReason().isPresent() && response.stopReason().get().equals(BetaStopReason.COMPACTION)) { // Preserve the prior exchange + current user message (3 messages) List preservedMessages = messages.size() >= 3 ? new ArrayList<>(messages.subList(messages.size() - 3, messages.size())) : new ArrayList<>(messages); // Build new message list: compaction + preserved messages List messagesAfterCompaction = new ArrayList<>(); messagesAfterCompaction.add(response.toParam()); messagesAfterCompaction.addAll(preservedMessages); // Continue the request with the compacted context + preserved messages MessageCreateParams continueParams = MessageCreateParams.builder() .addBeta("compact-2026-01-12") .model("claude-opus-5") .maxTokens(4096L) .messages(messagesAfterCompaction) .contextManagement(BetaContextManagementConfig.builder() .addEdit(BetaCompact20260112Edit.builder().build()) .build()) .build(); response = client.beta().messages().create(continueParams); // Update the message list to reflect the compaction messages.clear(); messages.addAll(messagesAfterCompaction); } // Append the final response messages.add(response.toParam()); return response.content().stream() .filter(block -> block.text().isPresent()) .map(block -> block.text().get().text()) .findFirst() .orElse(""); } public static void main(String[] args) { System.out.println(chat("Help me build a Python web scraper")); System.out.println(chat("Add support for JavaScript-rendered pages")); System.out.println(chat("Now add rate limiting and error handling")); } ``` ```php PHP $client = new Client(); $messages = []; function chat($client, &$messages, $userMessage) { $messages[] = ['role' => 'user', 'content' => $userMessage]; $response = $client->beta->messages->create( maxTokens: 4096, messages: $messages, model: 'claude-opus-5', betas: ['compact-2026-01-12'], contextManagement: [ 'edits' => [ [ 'type' => 'compact_20260112', 'trigger' => ['type' => 'input_tokens', 'value' => 100000], 'pause_after_compaction' => true ] ] ] ); if ($response->stopReason === 'compaction') { $compactionBlock = $response->content[0]; $preserved = count($messages) >= 3 ? array_slice($messages, -3) : $messages; $messagesAfterCompaction = array_merge( [['role' => 'assistant', 'content' => [$compactionBlock]]], $preserved ); $response = $client->beta->messages->create( maxTokens: 4096, messages: $messagesAfterCompaction, model: 'claude-opus-5', betas: ['compact-2026-01-12'], contextManagement: [ 'edits' => [['type' => 'compact_20260112']] ] ); $messages = $messagesAfterCompaction; } $messages[] = ['role' => 'assistant', 'content' => $response->content]; foreach ($response->content as $block) { if ($block->type === 'text') { return $block->text; } } return ''; } echo chat($client, $messages, "Help me build a Python web scraper") . "\n"; echo chat($client, $messages, "Add support for JavaScript-rendered pages") . "\n"; echo chat($client, $messages, "Now add rate limiting and error handling") . "\n"; ``` ```ruby Ruby client = Anthropic::Client.new messages = [] def chat(client, messages, user_message) messages << { role: "user", content: user_message } response = client.beta.messages.create( betas: ["compact-2026-01-12"], model: "claude-opus-5", max_tokens: 4096, messages: messages, context_management: { edits: [ { type: "compact_20260112", trigger: { type: "input_tokens", value: 100000 }, pause_after_compaction: true } ] } ) if response.stop_reason == :compaction compaction_block = response.content[0] preserved = messages.length >= 3 ? messages[-3..-1] : messages.dup messages_after_compaction = [ { role: "assistant", content: [compaction_block] } ] + preserved response = client.beta.messages.create( betas: ["compact-2026-01-12"], model: "claude-opus-5", max_tokens: 4096, messages: messages_after_compaction, context_management: { edits: [{ type: "compact_20260112" }] } ) messages.clear messages.concat(messages_after_compaction) end messages << { role: "assistant", content: response.content } response.content.find { |block| block.type == :text }&.text || "" end puts chat(client, messages, "Help me build a Python web scraper") puts chat(client, messages, "Add support for JavaScript-rendered pages") puts chat(client, messages, "Now add rate limiting and error handling") ``` ## Current limitations * **Same model for summarization:** The model specified in your request is used for summarization. There is no option to use a different (for example, cheaper) model for the summary. * **Compaction might fail when tools are defined:** When your request includes `tools`, the model occasionally calls a tool during the internal summarization step instead of writing a summary. When this occurs, the response contains a `compaction` block with `content: null`. To prevent this, set [`instructions`](https://platform.claude.com/docs/en/build-with-claude/compaction#custom-summarization-instructions) to a prompt that explicitly tells the model not to call tools, for example: ```text wrap Summarize the transcript inside tags. Include relevant information in the summary for continuing the task in the next context window. Do not call any tools while writing this summary; respond with text only. ``` ## Next steps Automatically manage conversation context as it grows with context editing. Learn about context window sizes and management strategies. Explore a practical implementation that manages long-running conversations with instant session memory compaction using background threading and prompt caching. --- title: Context editing url: https://platform.claude.com/docs/en/build-with-claude/context-editing description: Automatically manage conversation context as it grows with context editing. --- For how zero data retention (ZDR) applies to this feature, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention). ## Overview For most use cases, [server-side compaction](https://platform.claude.com/docs/en/build-with-claude/compaction) is the primary strategy for managing context in long-running conversations. The strategies on this page are useful for specific scenarios where you need more fine-grained control over what content is cleared. Context editing allows you to selectively clear specific content from conversation history as it grows. Beyond optimizing costs and staying within limits, this is about actively curating what Claude sees: context is a finite resource with diminishing returns, and irrelevant content degrades model focus. Context editing gives you fine-grained runtime control over that curation. For the broader principles behind context management, see [Effective context engineering](https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents). This page covers: * **Tool result clearing** - Best for agentic workflows with heavy tool use where old tool results are no longer needed * **Thinking block clearing** - For managing thinking blocks when using extended thinking, with options to preserve recent thinking for context continuity * **Client-side SDK compaction** - An SDK-based alternative for summary-based context management (server-side compaction is generally preferred) | Approach | Where it runs | Strategies | How it works | | --------------- | ------------- | ----------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Server-side** | API | Tool result clearing (`clear_tool_uses_20250919`) Thinking block clearing (`clear_thinking_20251015`) | Applied before the prompt reaches Claude. Clears specific content from conversation history. Each strategy can be configured independently. | | **Client-side** | SDK | Compaction | Available in [Python, TypeScript, and Ruby SDKs](https://platform.claude.com/docs/en/cli-sdks-libraries/overview) when using [`tool_runner`](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-runner). Generates a summary and replaces full conversation history. See [Client-side compaction](https://platform.claude.com/docs/en/build-with-claude/context-editing#client-side-compaction-sdk). | ## Server-side strategies Context editing is in beta with support for tool result clearing and thinking block clearing. To enable it, use the beta header `context-management-2025-06-27` in your API requests. Share feedback on this feature through the [feedback form](https://forms.gle/YXC2EKGMhjN1c4L88). ### Tool result clearing The `clear_tool_uses_20250919` strategy clears tool results when conversation context grows beyond your configured threshold. This is particularly useful for agentic workflows with heavy tool use. Older tool results (like file contents or search results) are no longer needed once Claude has processed them. When activated, the API automatically clears the oldest tool results in chronological order. The API replaces each cleared result with placeholder text indicating to Claude that it was removed. By default, only tool results are cleared. You can optionally clear both tool results and tool calls (the tool use parameters) by setting `clear_tool_inputs` to true. ### Thinking block clearing The `clear_thinking_20251015` strategy manages `thinking` blocks in conversations when extended thinking is enabled. This strategy gives you control over thinking preservation: you can choose to keep more thinking blocks to maintain reasoning continuity, or clear them more aggressively to save context space. **Default behavior:** The default varies by model class. | Model class | Keep all prior thinking | Keep only the last turn's thinking | | ----------- | --------------------------- | ----------------------------------- | | Opus | Claude Opus 4.5 and later | Claude Opus 4.1 and earlier | | Sonnet | Claude Sonnet 4.6 and later | Claude Sonnet 4.5 and earlier | | Haiku | (none) | All models through Claude Haiku 4.5 | Use this strategy to override the default. If your code runs across multiple model tiers, set `keep` explicitly rather than relying on the per-model default. An assistant conversation turn may include multiple content blocks (for example, when using tools) and multiple thinking blocks (for example, with [interleaved thinking](https://platform.claude.com/docs/en/build-with-claude/thinking#interleaved-thinking)). ### Context editing happens server-side Context editing is applied server-side before the prompt reaches Claude. Your client application maintains the full, unmodified conversation history. You do not need to sync your client state with the edited version. Continue managing your full conversation history locally as you normally would. ### Context editing and prompt caching Context editing's interaction with [prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) varies by strategy: * **Tool result clearing:** Invalidates cached prompt prefixes when content is cleared. To account for this, clear enough tokens to make the cache invalidation worthwhile. Use the `clear_at_least` parameter to ensure a minimum number of tokens is cleared each time. You'll incur cache write costs each time content is cleared, but subsequent requests can reuse the newly cached prefix. * **Thinking block clearing:** When thinking blocks are **kept** in context (not cleared), the prompt cache is preserved, enabling cache hits and reducing input token costs. When thinking blocks are **cleared**, the cache is invalidated at the point where clearing occurs. Configure the `keep` parameter based on whether you want to prioritize cache performance or context window availability. ## Supported models Context editing is available on all supported Claude models. ## Tool result clearing usage The simplest way to enable tool result clearing is to specify only the strategy type. All other [configuration options](https://platform.claude.com/docs/en/build-with-claude/context-editing#configuration-options-for-tool-result-clearing) use their default values: ```bash cURL curl https://api.anthropic.com/v1/messages \ --header "x-api-key: $ANTHROPIC_API_KEY" \ --header "anthropic-version: 2023-06-01" \ --header "content-type: application/json" \ --header "anthropic-beta: context-management-2025-06-27" \ --data '{ "model": "claude-opus-5", "max_tokens": 4096, "messages": [ { "role": "user", "content": "Search for recent developments in AI" } ], "tools": [ { "type": "web_search_20250305", "name": "web_search" } ], "context_management": { "edits": [ {"type": "clear_tool_uses_20250919"} ] } }' ``` ```bash CLI ant beta:messages create --beta context-management-2025-06-27 <<'YAML' model: claude-opus-5 max_tokens: 4096 messages: - role: user content: Search for recent developments in AI tools: - type: web_search_20250305 name: web_search context_management: edits: - type: clear_tool_uses_20250919 YAML ``` ```python Python response = client.beta.messages.create( model="claude-opus-5", max_tokens=4096, messages=[{"role": "user", "content": "Search for recent developments in AI"}], tools=[{"type": "web_search_20250305", "name": "web_search"}], betas=["context-management-2025-06-27"], context_management={"edits": [{"type": "clear_tool_uses_20250919"}]}, ) ``` ```typescript TypeScript const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }); const response = await anthropic.beta.messages.create({ model: "claude-opus-5", max_tokens: 4096, messages: [ { role: "user", content: "Search for recent developments in AI" } ], tools: [ { type: "web_search_20250305", name: "web_search" } ], context_management: { edits: [{ type: "clear_tool_uses_20250919" }] }, betas: ["context-management-2025-06-27"] }); ``` ```csharp C# using Anthropic; using Anthropic.Models.Beta; using Anthropic.Models.Beta.Messages; using Messages = Anthropic.Models.Messages; AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Messages::Model.ClaudeOpus5, MaxTokens = 4096, Messages = [ new() { Role = Role.User, Content = "Search for recent developments in AI" } ], Tools = [ new BetaWebSearchTool20250305() ], ContextManagement = new BetaContextManagementConfig { Edits = [new BetaClearToolUses20250919Edit()] }, Betas = [AnthropicBeta.ContextManagement2025_06_27] }; var response = await client.Beta.Messages.Create(parameters); Console.WriteLine(response); ``` ```go Go client := anthropic.NewClient() response, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 4096, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Search for recent developments in AI")), }, Tools: []anthropic.BetaToolUnionParam{ {OfWebSearchTool20250305: &anthropic.BetaWebSearchTool20250305Param{}}, }, ContextManagement: anthropic.BetaContextManagementConfigParam{ Edits: []anthropic.BetaContextManagementConfigEditUnionParam{ {OfClearToolUses20250919: &anthropic.BetaClearToolUses20250919EditParam{}}, }, }, Betas: []anthropic.AnthropicBeta{ anthropic.AnthropicBetaContextManagement2025_06_27, }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java import com.anthropic.models.beta.messages.BetaWebSearchTool20250305; import com.anthropic.models.beta.messages.BetaContextManagementConfig; import com.anthropic.models.beta.messages.BetaClearToolUses20250919Edit; import com.anthropic.models.beta.AnthropicBeta; // ... void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(4096L) .addUserMessage("Search for recent developments in AI") .addTool(BetaWebSearchTool20250305.builder().build()) .contextManagement(BetaContextManagementConfig.builder() .addEdit(BetaClearToolUses20250919Edit.builder().build()) .build()) .addBeta(AnthropicBeta.CONTEXT_MANAGEMENT_2025_06_27) .build(); BetaMessage response = client.beta().messages().create(params); IO.println(response); } ``` ```php PHP $client = new Client(); $response = $client->beta->messages->create( maxTokens: 4096, messages: [ ['role' => 'user', 'content' => 'Search for recent developments in AI'] ], model: 'claude-opus-5', betas: ['context-management-2025-06-27'], tools: [ ['type' => 'web_search_20250305', 'name' => 'web_search'] ], contextManagement: [ 'edits' => [ ['type' => 'clear_tool_uses_20250919'] ] ], ); echo $response; ``` ```ruby Ruby client = Anthropic::Client.new response = client.beta.messages.create( model: "claude-opus-5", max_tokens: 4096, messages: [ { role: "user", content: "Search for recent developments in AI" } ], tools: [ { type: "web_search_20250305", name: "web_search" } ], context_management: { edits: [ { type: "clear_tool_uses_20250919" } ] }, betas: ["context-management-2025-06-27"] ) puts response ``` ### Advanced configuration You can customize the tool result clearing behavior with additional parameters: ```bash cURL curl https://api.anthropic.com/v1/messages \ --header "x-api-key: $ANTHROPIC_API_KEY" \ --header "anthropic-version: 2023-06-01" \ --header "content-type: application/json" \ --header "anthropic-beta: context-management-2025-06-27" \ --data '{ "model": "claude-opus-5", "max_tokens": 4096, "messages": [ { "role": "user", "content": "Create a simple command line calculator app using Python" } ], "tools": [ { "type": "text_editor_20250728", "name": "str_replace_based_edit_tool", "max_characters": 10000 }, { "type": "web_search_20250305", "name": "web_search", "max_uses": 3 } ], "context_management": { "edits": [ { "type": "clear_tool_uses_20250919", "trigger": { "type": "input_tokens", "value": 30000 }, "keep": { "type": "tool_uses", "value": 3 }, "clear_at_least": { "type": "input_tokens", "value": 5000 }, "exclude_tools": ["web_search"] } ] } }' ``` ```bash CLI ant beta:messages create --beta context-management-2025-06-27 <<'YAML' model: claude-opus-5 max_tokens: 4096 messages: - role: user content: Create a simple command line calculator app using Python tools: - type: text_editor_20250728 name: str_replace_based_edit_tool max_characters: 10000 - type: web_search_20250305 name: web_search max_uses: 3 context_management: edits: - type: clear_tool_uses_20250919 trigger: type: input_tokens value: 30000 keep: type: tool_uses value: 3 clear_at_least: type: input_tokens value: 5000 exclude_tools: - web_search YAML ``` ```python Python response = client.beta.messages.create( model="claude-opus-5", max_tokens=4096, messages=[ { "role": "user", "content": "Create a simple command line calculator app using Python", } ], tools=[ { "type": "text_editor_20250728", "name": "str_replace_based_edit_tool", "max_characters": 10000, }, {"type": "web_search_20250305", "name": "web_search", "max_uses": 3}, ], betas=["context-management-2025-06-27"], context_management={ "edits": [ { "type": "clear_tool_uses_20250919", # Trigger clearing when threshold is exceeded "trigger": {"type": "input_tokens", "value": 30000}, # Number of tool uses to keep after clearing "keep": {"type": "tool_uses", "value": 3}, # Optional: Clear at least this many tokens "clear_at_least": {"type": "input_tokens", "value": 5000}, # Exclude these tools from being cleared "exclude_tools": ["web_search"], } ] }, ) ``` ```typescript TypeScript const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }); const response = await anthropic.beta.messages.create({ model: "claude-opus-5", max_tokens: 4096, messages: [ { role: "user", content: "Create a simple command line calculator app using Python" } ], tools: [ { type: "text_editor_20250728", name: "str_replace_based_edit_tool", max_characters: 10000 }, { type: "web_search_20250305", name: "web_search", max_uses: 3 } ], betas: ["context-management-2025-06-27"], context_management: { edits: [ { type: "clear_tool_uses_20250919", // Trigger clearing when threshold is exceeded trigger: { type: "input_tokens", value: 30000 }, // Number of tool uses to keep after clearing keep: { type: "tool_uses", value: 3 }, // Optional: Clear at least this many tokens clear_at_least: { type: "input_tokens", value: 5000 }, // Exclude these tools from being cleared exclude_tools: ["web_search"] } ] } }); ``` ```csharp C# using Anthropic; using Anthropic.Models.Beta; using Anthropic.Models.Beta.Messages; using Messages = Anthropic.Models.Messages; AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Messages::Model.ClaudeOpus5, MaxTokens = 4096, Messages = [ new() { Role = Role.User, Content = "Create a simple command line calculator app using Python" } ], Tools = [ new BetaToolTextEditor20250728 { MaxCharacters = 10000 }, new BetaWebSearchTool20250305 { MaxUses = 3 } ], Betas = [AnthropicBeta.ContextManagement2025_06_27], ContextManagement = new BetaContextManagementConfig { Edits = [ new BetaClearToolUses20250919Edit { Trigger = new BetaInputTokensTrigger(30000), Keep = new BetaToolUsesKeep(3), ClearAtLeast = new BetaInputTokensClearAtLeast(5000), ExcludeTools = ["web_search"] } ] } }; var response = await client.Beta.Messages.Create(parameters); Console.WriteLine(response); ``` ```go Go client := anthropic.NewClient() response, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 4096, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Create a simple command line calculator app using Python")), }, Tools: []anthropic.BetaToolUnionParam{ {OfTextEditor20250728: &anthropic.BetaToolTextEditor20250728Param{ MaxCharacters: anthropic.Int(10000), }}, {OfWebSearchTool20250305: &anthropic.BetaWebSearchTool20250305Param{ MaxUses: anthropic.Int(3), }}, }, Betas: []anthropic.AnthropicBeta{anthropic.AnthropicBetaContextManagement2025_06_27}, ContextManagement: anthropic.BetaContextManagementConfigParam{ Edits: []anthropic.BetaContextManagementConfigEditUnionParam{ {OfClearToolUses20250919: &anthropic.BetaClearToolUses20250919EditParam{ Trigger: anthropic.BetaClearToolUses20250919EditTriggerUnionParam{ OfInputTokens: &anthropic.BetaInputTokensTriggerParam{ Value: 30000, }, }, Keep: anthropic.BetaToolUsesKeepParam{ Value: 3, }, ClearAtLeast: anthropic.BetaInputTokensClearAtLeastParam{ Value: 5000, }, ExcludeTools: []string{"web_search"}, }}, }, }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java import com.anthropic.models.beta.messages.BetaToolTextEditor20250728; import com.anthropic.models.beta.messages.BetaWebSearchTool20250305; import com.anthropic.models.beta.messages.BetaContextManagementConfig; import com.anthropic.models.beta.messages.BetaClearToolUses20250919Edit; import com.anthropic.models.beta.messages.BetaInputTokensTrigger; import com.anthropic.models.beta.messages.BetaInputTokensClearAtLeast; import com.anthropic.models.beta.messages.BetaToolUsesKeep; import com.anthropic.models.beta.AnthropicBeta; // ... void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(4096L) .addUserMessage("Create a simple command line calculator app using Python") .addTool(BetaToolTextEditor20250728.builder() .maxCharacters(10000L) .build()) .addTool(BetaWebSearchTool20250305.builder() .maxUses(3L) .build()) .addBeta(AnthropicBeta.CONTEXT_MANAGEMENT_2025_06_27) .contextManagement(BetaContextManagementConfig.builder() .addEdit(BetaClearToolUses20250919Edit.builder() .trigger(BetaInputTokensTrigger.builder() .value(30000L) .build()) .keep(BetaToolUsesKeep.builder() .value(3L) .build()) .clearAtLeast(BetaInputTokensClearAtLeast.builder() .value(5000L) .build()) .addExcludeTool("web_search") .build()) .build()) .build(); BetaMessage response = client.beta().messages().create(params); IO.println(response); } ``` ```php PHP $client = new Client(); $response = $client->beta->messages->create( maxTokens: 4096, messages: [ [ 'role' => 'user', 'content' => 'Create a simple command line calculator app using Python' ] ], model: 'claude-opus-5', betas: ['context-management-2025-06-27'], tools: [ [ 'type' => 'text_editor_20250728', 'name' => 'str_replace_based_edit_tool', 'max_characters' => 10000 ], [ 'type' => 'web_search_20250305', 'name' => 'web_search', 'max_uses' => 3 ] ], contextManagement: [ 'edits' => [ [ 'type' => 'clear_tool_uses_20250919', 'trigger' => [ 'type' => 'input_tokens', 'value' => 30000 ], 'keep' => [ 'type' => 'tool_uses', 'value' => 3 ], 'clear_at_least' => [ 'type' => 'input_tokens', 'value' => 5000 ], 'exclude_tools' => ['web_search'] ] ] ], ); echo $response; ``` ```ruby Ruby client = Anthropic::Client.new response = client.beta.messages.create( model: "claude-opus-5", max_tokens: 4096, messages: [ { role: "user", content: "Create a simple command line calculator app using Python" } ], tools: [ { type: "text_editor_20250728", name: "str_replace_based_edit_tool", max_characters: 10000 }, { type: "web_search_20250305", name: "web_search", max_uses: 3 } ], betas: ["context-management-2025-06-27"], context_management: { edits: [ { type: "clear_tool_uses_20250919", trigger: { type: "input_tokens", value: 30000 }, keep: { type: "tool_uses", value: 3 }, clear_at_least: { type: "input_tokens", value: 5000 }, exclude_tools: ["web_search"] } ] } ) puts response ``` ## Thinking block clearing usage Enable thinking block clearing to manage context and prompt caching effectively when extended thinking is enabled: ```bash cURL curl https://api.anthropic.com/v1/messages \ --header "x-api-key: $ANTHROPIC_API_KEY" \ --header "anthropic-version: 2023-06-01" \ --header "content-type: application/json" \ --header "anthropic-beta: context-management-2025-06-27" \ --data '{ "model": "claude-opus-5", "max_tokens": 16000, "messages": [{"role": "user", "content": "Hello"}], "context_management": { "edits": [ { "type": "clear_thinking_20251015", "keep": { "type": "thinking_turns", "value": 2 } } ] } }' ``` ```bash CLI ant beta:messages create --beta context-management-2025-06-27 <<'YAML' model: claude-opus-5 max_tokens: 16000 messages: - role: user content: Hello context_management: edits: - type: clear_thinking_20251015 keep: type: thinking_turns value: 2 YAML ``` ```python Python response = client.beta.messages.create( model="claude-opus-5", max_tokens=16000, messages=[{"role": "user", "content": "Hello"}], betas=["context-management-2025-06-27"], context_management={ "edits": [ { "type": "clear_thinking_20251015", "keep": {"type": "thinking_turns", "value": 2}, } ] }, ) ``` ```typescript TypeScript const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }); const response = await anthropic.beta.messages.create({ model: "claude-opus-5", max_tokens: 16000, messages: [{ role: "user", content: "Hello" }], betas: ["context-management-2025-06-27"], context_management: { edits: [ { type: "clear_thinking_20251015", keep: { type: "thinking_turns", value: 2 } } ] } }); ``` ```csharp C# using Anthropic; using Anthropic.Models.Beta; using Anthropic.Models.Beta.Messages; using Messages = Anthropic.Models.Messages; AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Messages::Model.ClaudeOpus5, MaxTokens = 16000, Messages = [ new() { Role = Role.User, Content = "Hello" } ], Betas = [AnthropicBeta.ContextManagement2025_06_27], ContextManagement = new BetaContextManagementConfig { Edits = [ new BetaClearThinking20251015Edit { Keep = new BetaThinkingTurns(2) } ] } }; var response = await client.Beta.Messages.Create(parameters); Console.WriteLine(response); ``` ```go Go client := anthropic.NewClient() response, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 16000, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Hello")), }, Betas: []anthropic.AnthropicBeta{anthropic.AnthropicBetaContextManagement2025_06_27}, ContextManagement: anthropic.BetaContextManagementConfigParam{ Edits: []anthropic.BetaContextManagementConfigEditUnionParam{ {OfClearThinking20251015: &anthropic.BetaClearThinking20251015EditParam{ Keep: anthropic.BetaClearThinking20251015EditKeepUnionParam{ OfThinkingTurns: &anthropic.BetaThinkingTurnsParam{ Value: 2, }, }, }}, }, }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java import com.anthropic.models.beta.messages.BetaContextManagementConfig; import com.anthropic.models.beta.messages.BetaClearThinking20251015Edit; import com.anthropic.models.beta.messages.BetaThinkingTurns; import com.anthropic.models.beta.AnthropicBeta; // ... void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(16000L) .addUserMessage("Hello") .addBeta(AnthropicBeta.CONTEXT_MANAGEMENT_2025_06_27) .contextManagement(BetaContextManagementConfig.builder() .addEdit(BetaClearThinking20251015Edit.builder() .keep(BetaThinkingTurns.builder() .value(2L) .build()) .build()) .build()) .build(); BetaMessage response = client.beta().messages().create(params); IO.println(response); } ``` ```php PHP $client = new Client(); $response = $client->beta->messages->create( maxTokens: 16000, messages: [ ['role' => 'user', 'content' => 'Hello'] ], model: 'claude-opus-5', betas: ['context-management-2025-06-27'], contextManagement: [ 'edits' => [ [ 'type' => 'clear_thinking_20251015', 'keep' => [ 'type' => 'thinking_turns', 'value' => 2 ] ] ] ], ); echo $response; ``` ```ruby Ruby client = Anthropic::Client.new response = client.beta.messages.create( model: "claude-opus-5", max_tokens: 16000, messages: [{ role: "user", content: "Hello" }], betas: ["context-management-2025-06-27"], context_management: { edits: [ { type: "clear_thinking_20251015", keep: { type: "thinking_turns", value: 2 } } ] } ) puts response ``` ### Configuration options for thinking block clearing The `clear_thinking_20251015` strategy supports the following configuration: | Configuration option | Default | Description | | -------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `keep` | Model-specific | Defines how many recent assistant turns with thinking blocks to preserve. Use `{type: "thinking_turns", value: N}` where N must be > 0 to keep the last N turns, or `"all"` to keep all thinking blocks. Opus 4.5+ and Sonnet 4.6+: all turns. Earlier Opus/Sonnet and all Haiku: last turn only. | **Example configurations:** Keep thinking blocks from the last 3 assistant turns: ```bash cURL curl https://api.anthropic.com/v1/messages \ --header "x-api-key: $ANTHROPIC_API_KEY" \ --header "anthropic-version: 2023-06-01" \ --header "content-type: application/json" \ --header "anthropic-beta: context-management-2025-06-27" \ --data '{ "model": "claude-opus-5", "max_tokens": 16000, "messages": [{"role": "user", "content": "Hello"}], "context_management": { "edits": [ { "type": "clear_thinking_20251015", "keep": { "type": "thinking_turns", "value": 3 } } ] } }' ``` ```bash CLI ant beta:messages create --beta context-management-2025-06-27 <<'YAML' model: claude-opus-5 max_tokens: 16000 messages: - role: user content: Hello context_management: edits: - type: clear_thinking_20251015 keep: type: thinking_turns value: 3 YAML ``` ```python Python response = client.beta.messages.create( model="claude-opus-5", max_tokens=16000, messages=[{"role": "user", "content": "Hello"}], betas=["context-management-2025-06-27"], context_management={ "edits": [ { "type": "clear_thinking_20251015", "keep": {"type": "thinking_turns", "value": 3}, } ] }, ) ``` ```typescript TypeScript const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }); const response = await anthropic.beta.messages.create({ model: "claude-opus-5", max_tokens: 16000, messages: [{ role: "user", content: "Hello" }], betas: ["context-management-2025-06-27"], context_management: { edits: [ { type: "clear_thinking_20251015", keep: { type: "thinking_turns", value: 3 } } ] } }); ``` ```csharp C# using Anthropic; using Anthropic.Models.Beta; using Anthropic.Models.Beta.Messages; using Messages = Anthropic.Models.Messages; AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Messages::Model.ClaudeOpus5, MaxTokens = 16000, Messages = [ new() { Role = Role.User, Content = "Hello" } ], Betas = [AnthropicBeta.ContextManagement2025_06_27], ContextManagement = new BetaContextManagementConfig { Edits = [ new BetaClearThinking20251015Edit { Keep = new BetaThinkingTurns(3) } ] } }; var response = await client.Beta.Messages.Create(parameters); Console.WriteLine(response); ``` ```go Go client := anthropic.NewClient() response, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 16000, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Hello")), }, Betas: []anthropic.AnthropicBeta{anthropic.AnthropicBetaContextManagement2025_06_27}, ContextManagement: anthropic.BetaContextManagementConfigParam{ Edits: []anthropic.BetaContextManagementConfigEditUnionParam{ {OfClearThinking20251015: &anthropic.BetaClearThinking20251015EditParam{ Keep: anthropic.BetaClearThinking20251015EditKeepUnionParam{ OfThinkingTurns: &anthropic.BetaThinkingTurnsParam{ Value: 3, }, }, }}, }, }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(16000L) .addUserMessage("Hello") .addBeta(AnthropicBeta.CONTEXT_MANAGEMENT_2025_06_27) .contextManagement(BetaContextManagementConfig.builder() .addEdit(BetaClearThinking20251015Edit.builder() .keep(BetaThinkingTurns.builder() .value(3L) .build()) .build()) .build()) .build(); BetaMessage response = client.beta().messages().create(params); IO.println(response); ``` ```php PHP $client = new Client(); $response = $client->beta->messages->create( maxTokens: 16000, messages: [ ['role' => 'user', 'content' => 'Hello'] ], model: 'claude-opus-5', betas: ['context-management-2025-06-27'], contextManagement: [ 'edits' => [ [ 'type' => 'clear_thinking_20251015', 'keep' => [ 'type' => 'thinking_turns', 'value' => 3 ] ] ] ], ); echo $response; ``` ```ruby Ruby client = Anthropic::Client.new response = client.beta.messages.create( model: "claude-opus-5", max_tokens: 16000, messages: [{ role: "user", content: "Hello" }], betas: ["context-management-2025-06-27"], context_management: { edits: [ { type: "clear_thinking_20251015", keep: { type: "thinking_turns", value: 3 } } ] } ) puts response ``` Keep all thinking blocks (maximizes cache hits): ```bash cURL curl https://api.anthropic.com/v1/messages \ --header "x-api-key: $ANTHROPIC_API_KEY" \ --header "anthropic-version: 2023-06-01" \ --header "content-type: application/json" \ --header "anthropic-beta: context-management-2025-06-27" \ --data '{ "model": "claude-opus-5", "max_tokens": 16000, "messages": [{"role": "user", "content": "Hello"}], "context_management": { "edits": [ { "type": "clear_thinking_20251015", "keep": "all" } ] } }' ``` ```bash CLI ant beta:messages create --beta context-management-2025-06-27 <<'YAML' model: claude-opus-5 max_tokens: 16000 messages: - role: user content: Hello context_management: edits: - type: clear_thinking_20251015 keep: all YAML ``` ```python Python response = client.beta.messages.create( model="claude-opus-5", max_tokens=16000, messages=[{"role": "user", "content": "Hello"}], betas=["context-management-2025-06-27"], context_management={ "edits": [ { "type": "clear_thinking_20251015", "keep": "all", } ] }, ) ``` ```typescript TypeScript const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }); const response = await anthropic.beta.messages.create({ model: "claude-opus-5", max_tokens: 16000, messages: [{ role: "user", content: "Hello" }], betas: ["context-management-2025-06-27"], context_management: { edits: [ { type: "clear_thinking_20251015", keep: "all" } ] } }); ``` ```csharp C# using Anthropic; using Anthropic.Models.Beta; using Anthropic.Models.Beta.Messages; using Messages = Anthropic.Models.Messages; AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Messages::Model.ClaudeOpus5, MaxTokens = 16000, Messages = [ new() { Role = Role.User, Content = "Hello" } ], Betas = [AnthropicBeta.ContextManagement2025_06_27], ContextManagement = new BetaContextManagementConfig { Edits = [ new BetaClearThinking20251015Edit { Keep = new All() } ] } }; var response = await client.Beta.Messages.Create(parameters); Console.WriteLine(response); ``` ```go Go client := anthropic.NewClient() response, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 16000, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Hello")), }, Betas: []anthropic.AnthropicBeta{anthropic.AnthropicBetaContextManagement2025_06_27}, ContextManagement: anthropic.BetaContextManagementConfigParam{ Edits: []anthropic.BetaContextManagementConfigEditUnionParam{ {OfClearThinking20251015: &anthropic.BetaClearThinking20251015EditParam{ Keep: anthropic.BetaClearThinking20251015EditKeepUnionParam{ OfAll: constant.ValueOf[constant.All](), }, }}, }, }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(16000L) .addUserMessage("Hello") .addBeta(AnthropicBeta.CONTEXT_MANAGEMENT_2025_06_27) .contextManagement(BetaContextManagementConfig.builder() .addEdit(BetaClearThinking20251015Edit.builder() .keepAll() .build()) .build()) .build(); BetaMessage response = client.beta().messages().create(params); IO.println(response); ``` ```php PHP $client = new Client(); $response = $client->beta->messages->create( maxTokens: 16000, messages: [ ['role' => 'user', 'content' => 'Hello'] ], model: 'claude-opus-5', betas: ['context-management-2025-06-27'], contextManagement: [ 'edits' => [ [ 'type' => 'clear_thinking_20251015', 'keep' => 'all' ] ] ], ); echo $response; ``` ```ruby Ruby client = Anthropic::Client.new response = client.beta.messages.create( model: "claude-opus-5", max_tokens: 16000, messages: [{ role: "user", content: "Hello" }], betas: ["context-management-2025-06-27"], context_management: { edits: [ { type: "clear_thinking_20251015", keep: "all" } ] } ) puts response ``` ### Combining strategies You can use both thinking block clearing and tool result clearing together: When using multiple strategies, the `clear_thinking_20251015` strategy must be listed first in the `edits` array. ```bash cURL curl https://api.anthropic.com/v1/messages \ --header "x-api-key: $ANTHROPIC_API_KEY" \ --header "anthropic-version: 2023-06-01" \ --header "content-type: application/json" \ --header "anthropic-beta: context-management-2025-06-27" \ --data '{ "model": "claude-opus-5", "max_tokens": 16000, "messages": [ { "role": "user", "content": "Search for the latest developments in quantum error correction and summarize the key breakthroughs." } ], "tools": [ { "type": "web_search_20250305", "name": "web_search", "max_uses": 5 } ], "context_management": { "edits": [ { "type": "clear_thinking_20251015", "keep": { "type": "thinking_turns", "value": 2 } }, { "type": "clear_tool_uses_20250919", "trigger": { "type": "input_tokens", "value": 50000 }, "keep": { "type": "tool_uses", "value": 5 } } ] } }' ``` ```bash CLI ant beta:messages create --beta context-management-2025-06-27 <<'YAML' model: claude-opus-5 max_tokens: 16000 messages: - role: user content: Search for the latest developments in quantum error correction and summarize the key breakthroughs. tools: - type: web_search_20250305 name: web_search max_uses: 5 context_management: edits: - type: clear_thinking_20251015 keep: type: thinking_turns value: 2 - type: clear_tool_uses_20250919 trigger: type: input_tokens value: 50000 keep: type: tool_uses value: 5 YAML ``` ```python Python response = client.beta.messages.create( model="claude-opus-5", max_tokens=16000, messages=[ { "role": "user", "content": "Search for the latest developments in quantum error correction and summarize the key breakthroughs.", } ], tools=[ { "type": "web_search_20250305", "name": "web_search", "max_uses": 5, } ], betas=["context-management-2025-06-27"], context_management={ "edits": [ { "type": "clear_thinking_20251015", "keep": {"type": "thinking_turns", "value": 2}, }, { "type": "clear_tool_uses_20250919", "trigger": {"type": "input_tokens", "value": 50000}, "keep": {"type": "tool_uses", "value": 5}, }, ] }, ) print(response) ``` ```typescript TypeScript const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }); const response = await anthropic.beta.messages.create({ model: "claude-opus-5", max_tokens: 16000, messages: [ { role: "user", content: "Search for the latest developments in quantum error correction and summarize the key breakthroughs." } ], tools: [ { type: "web_search_20250305", name: "web_search", max_uses: 5 } ], betas: ["context-management-2025-06-27"], context_management: { edits: [ { type: "clear_thinking_20251015", keep: { type: "thinking_turns", value: 2 } }, { type: "clear_tool_uses_20250919", trigger: { type: "input_tokens", value: 50000 }, keep: { type: "tool_uses", value: 5 } } ] } }); console.log(response); ``` ```csharp C# using Anthropic; using Anthropic.Models.Beta; using Anthropic.Models.Beta.Messages; using Messages = Anthropic.Models.Messages; AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Messages::Model.ClaudeOpus5, MaxTokens = 16000, Messages = [ new() { Role = Role.User, Content = "Search for the latest developments in quantum error correction and summarize the key breakthroughs." } ], Tools = [ new BetaWebSearchTool20250305 { MaxUses = 5 } ], Betas = [AnthropicBeta.ContextManagement2025_06_27], ContextManagement = new BetaContextManagementConfig { Edits = [ new BetaClearThinking20251015Edit { Keep = new BetaThinkingTurns(2) }, new BetaClearToolUses20250919Edit { Trigger = new BetaInputTokensTrigger(50000), Keep = new BetaToolUsesKeep(5) } ] } }; var response = await client.Beta.Messages.Create(parameters); Console.WriteLine(response); ``` ```go Go client := anthropic.NewClient() response, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 16000, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Search for the latest developments in quantum error correction and summarize the key breakthroughs.")), }, Tools: []anthropic.BetaToolUnionParam{ {OfWebSearchTool20250305: &anthropic.BetaWebSearchTool20250305Param{ MaxUses: anthropic.Int(5), }}, }, Betas: []anthropic.AnthropicBeta{ anthropic.AnthropicBetaContextManagement2025_06_27, }, ContextManagement: anthropic.BetaContextManagementConfigParam{ Edits: []anthropic.BetaContextManagementConfigEditUnionParam{ {OfClearThinking20251015: &anthropic.BetaClearThinking20251015EditParam{ Keep: anthropic.BetaClearThinking20251015EditKeepUnionParam{ OfThinkingTurns: &anthropic.BetaThinkingTurnsParam{ Value: 2, }, }, }}, {OfClearToolUses20250919: &anthropic.BetaClearToolUses20250919EditParam{ Trigger: anthropic.BetaClearToolUses20250919EditTriggerUnionParam{ OfInputTokens: &anthropic.BetaInputTokensTriggerParam{ Value: 50000, }, }, Keep: anthropic.BetaToolUsesKeepParam{ Value: 5, }, }}, }, }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java import com.anthropic.models.beta.messages.BetaWebSearchTool20250305; import com.anthropic.models.beta.messages.BetaContextManagementConfig; import com.anthropic.models.beta.messages.BetaClearThinking20251015Edit; import com.anthropic.models.beta.messages.BetaClearToolUses20250919Edit; import com.anthropic.models.beta.messages.BetaThinkingTurns; import com.anthropic.models.beta.messages.BetaInputTokensTrigger; import com.anthropic.models.beta.messages.BetaToolUsesKeep; import com.anthropic.models.beta.AnthropicBeta; // ... void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(16000L) .addUserMessage("Search for the latest developments in quantum error correction and summarize the key breakthroughs.") .addTool(BetaWebSearchTool20250305.builder() .maxUses(5L) .build()) .addBeta(AnthropicBeta.CONTEXT_MANAGEMENT_2025_06_27) .contextManagement(BetaContextManagementConfig.builder() .addEdit(BetaClearThinking20251015Edit.builder() .keep(BetaThinkingTurns.builder() .value(2L) .build()) .build()) .addEdit(BetaClearToolUses20250919Edit.builder() .trigger(BetaInputTokensTrigger.builder() .value(50000L) .build()) .keep(BetaToolUsesKeep.builder() .value(5L) .build()) .build()) .build()) .build(); BetaMessage response = client.beta().messages().create(params); IO.println(response); } ``` ```php PHP $client = new Client(); $response = $client->beta->messages->create( maxTokens: 16000, messages: [ [ 'role' => 'user', 'content' => 'Search for the latest developments in quantum error correction and summarize the key breakthroughs.' ] ], model: 'claude-opus-5', betas: ['context-management-2025-06-27'], tools: [ [ 'type' => 'web_search_20250305', 'name' => 'web_search', 'max_uses' => 5 ] ], contextManagement: [ 'edits' => [ [ 'type' => 'clear_thinking_20251015', 'keep' => [ 'type' => 'thinking_turns', 'value' => 2 ] ], [ 'type' => 'clear_tool_uses_20250919', 'trigger' => [ 'type' => 'input_tokens', 'value' => 50000 ], 'keep' => [ 'type' => 'tool_uses', 'value' => 5 ] ] ] ], ); echo $response; ``` ```ruby Ruby client = Anthropic::Client.new response = client.beta.messages.create( model: "claude-opus-5", max_tokens: 16000, messages: [ { role: "user", content: "Search for the latest developments in quantum error correction and summarize the key breakthroughs." } ], tools: [ { type: "web_search_20250305", name: "web_search", max_uses: 5 } ], betas: ["context-management-2025-06-27"], context_management: { edits: [ { type: "clear_thinking_20251015", keep: { type: "thinking_turns", value: 2 } }, { type: "clear_tool_uses_20250919", trigger: { type: "input_tokens", value: 50000 }, keep: { type: "tool_uses", value: 5 } } ] } ) puts response ``` ## Configuration options for tool result clearing | Configuration option | Default | Description | | -------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `trigger` | 100,000 input tokens | Defines when the context editing strategy activates. Once the prompt exceeds this threshold, clearing begins. You can specify this value in either `input_tokens` or `tool_uses`. | | `keep` | 3 tool uses | Defines how many recent tool use/result pairs to keep after clearing occurs. The API removes the oldest tool interactions first, preserving the most recent ones. | | `clear_at_least` | None | Ensures a minimum number of tokens is cleared each time the strategy activates. If the API can't clear at least the specified amount, the strategy will not be applied. This helps determine if context clearing is worth breaking your prompt cache. | | `exclude_tools` | None | List of tool names whose tool uses and results should never be cleared. Useful for preserving important context. | | `clear_tool_inputs` | `false` | Controls whether the tool call parameters are cleared along with the tool results. By default, only the tool results are cleared while keeping Claude's original tool calls visible. | ## Context editing response You can see which context edits were applied to your request using the `context_management` response field, along with helpful statistics about the content and input tokens cleared. ```json Output { "id": "msg_013Zva2CMHLNnXjNJJKqJ2EF", "type": "message", "role": "assistant", "content": [ // ... ], "usage": { // ... }, "context_management": { "applied_edits": [ // When using `clear_thinking_20251015` { "type": "clear_thinking_20251015", "cleared_thinking_turns": 3, "cleared_input_tokens": 15000 }, // When using `clear_tool_uses_20250919` { "type": "clear_tool_uses_20250919", "cleared_tool_uses": 8, "cleared_input_tokens": 50000 } ] } } ``` For streaming responses, the context edits are included in the final `message_delta` event: ```json Streaming Response { "type": "message_delta", "delta": { "stop_reason": "end_turn", "stop_sequence": null }, "usage": { "output_tokens": 1024 }, "context_management": { "applied_edits": [ // ... ] } } ``` ## Token counting The [token counting](https://platform.claude.com/docs/en/build-with-claude/token-counting) endpoint supports context management, allowing you to preview how many tokens your prompt will use after context editing is applied. ```bash cURL curl https://api.anthropic.com/v1/messages/count_tokens \ --header "x-api-key: $ANTHROPIC_API_KEY" \ --header "anthropic-version: 2023-06-01" \ --header "content-type: application/json" \ --header "anthropic-beta: context-management-2025-06-27" \ --data '{ "model": "claude-opus-5", "messages": [ { "role": "user", "content": "Continue our conversation..." } ], "context_management": { "edits": [ { "type": "clear_tool_uses_20250919", "trigger": { "type": "input_tokens", "value": 30000 }, "keep": { "type": "tool_uses", "value": 5 } } ] } }' ``` ```bash CLI cat > request.yaml <<'YAML' model: claude-opus-5 messages: - role: user content: Continue our conversation... context_management: edits: - type: clear_tool_uses_20250919 trigger: type: input_tokens value: 30000 keep: type: tool_uses value: 5 YAML ORIGINAL=$(ant beta:messages count-tokens \ --beta context-management-2025-06-27 \ --transform context_management.original_input_tokens \ --raw-output < request.yaml) INPUT_TOKENS=$(ant beta:messages count-tokens \ --beta context-management-2025-06-27 \ --transform input_tokens --raw-output < request.yaml) printf 'Original tokens: %s\n' "$ORIGINAL" printf 'After clearing: %s\n' "$INPUT_TOKENS" printf 'Savings: %s tokens\n' "$((ORIGINAL - INPUT_TOKENS))" ``` ```python Python response = client.beta.messages.count_tokens( model="claude-opus-5", messages=[{"role": "user", "content": "Continue our conversation..."}], betas=["context-management-2025-06-27"], context_management={ "edits": [ { "type": "clear_tool_uses_20250919", "trigger": {"type": "input_tokens", "value": 30000}, "keep": {"type": "tool_uses", "value": 5}, } ] }, ) print(f"Original tokens: {response.context_management.original_input_tokens}") print(f"After clearing: {response.input_tokens}") print( f"Savings: {response.context_management.original_input_tokens - response.input_tokens} tokens" ) ``` ```typescript TypeScript const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }); const response = await anthropic.beta.messages.countTokens({ model: "claude-opus-5", messages: [ { role: "user", content: "Continue our conversation..." } ], betas: ["context-management-2025-06-27"], context_management: { edits: [ { type: "clear_tool_uses_20250919", trigger: { type: "input_tokens", value: 30000 }, keep: { type: "tool_uses", value: 5 } } ] } }); console.log(`Original tokens: ${response.context_management?.original_input_tokens}`); console.log(`After clearing: ${response.input_tokens}`); console.log( `Savings: ${ (response.context_management?.original_input_tokens || 0) - response.input_tokens } tokens` ); ``` ```csharp C# using Anthropic; using Anthropic.Models.Beta; using Anthropic.Models.Beta.Messages; using Messages = Anthropic.Models.Messages; AnthropicClient client = new(); var parameters = new MessageCountTokensParams { Model = Messages::Model.ClaudeOpus5, Messages = [new() { Role = Role.User, Content = "Continue our conversation..." }], Betas = [AnthropicBeta.ContextManagement2025_06_27], ContextManagement = new BetaContextManagementConfig { Edits = [ new BetaClearToolUses20250919Edit { Trigger = new BetaInputTokensTrigger(30000), Keep = new BetaToolUsesKeep(5) } ] } }; var response = await client.Beta.Messages.CountTokens(parameters); Console.WriteLine($"Original tokens: {response.ContextManagement?.OriginalInputTokens}"); Console.WriteLine($"After clearing: {response.InputTokens}"); Console.WriteLine($"Savings: {(response.ContextManagement?.OriginalInputTokens ?? 0) - response.InputTokens} tokens"); ``` ```go Go client := anthropic.NewClient() response, err := client.Beta.Messages.CountTokens(context.TODO(), anthropic.BetaMessageCountTokensParams{ Model: anthropic.ModelClaudeOpus5, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Continue our conversation...")), }, Betas: []anthropic.AnthropicBeta{ anthropic.AnthropicBetaContextManagement2025_06_27, }, ContextManagement: anthropic.BetaContextManagementConfigParam{ Edits: []anthropic.BetaContextManagementConfigEditUnionParam{ {OfClearToolUses20250919: &anthropic.BetaClearToolUses20250919EditParam{ Trigger: anthropic.BetaClearToolUses20250919EditTriggerUnionParam{ OfInputTokens: &anthropic.BetaInputTokensTriggerParam{ Value: 30000, }, }, Keep: anthropic.BetaToolUsesKeepParam{ Value: 5, }, }}, }, }, }) if err != nil { log.Fatal(err) } fmt.Printf("Original tokens: %d\n", response.ContextManagement.OriginalInputTokens) fmt.Printf("After clearing: %d\n", response.InputTokens) fmt.Printf("Savings: %d tokens\n", response.ContextManagement.OriginalInputTokens-response.InputTokens) ``` ```java Java import com.anthropic.models.beta.messages.BetaMessageTokensCount; import com.anthropic.models.beta.messages.MessageCountTokensParams; import com.anthropic.models.beta.messages.BetaContextManagementConfig; import com.anthropic.models.beta.messages.BetaClearToolUses20250919Edit; import com.anthropic.models.beta.messages.BetaInputTokensTrigger; import com.anthropic.models.beta.messages.BetaToolUsesKeep; import com.anthropic.models.beta.AnthropicBeta; // ... void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCountTokensParams params = MessageCountTokensParams.builder() .model(Model.CLAUDE_OPUS_5) .addUserMessage("Continue our conversation...") .addBeta(AnthropicBeta.CONTEXT_MANAGEMENT_2025_06_27) .contextManagement(BetaContextManagementConfig.builder() .addEdit(BetaClearToolUses20250919Edit.builder() .trigger(BetaInputTokensTrigger.builder() .value(30000L) .build()) .keep(BetaToolUsesKeep.builder() .value(5L) .build()) .build()) .build()) .build(); BetaMessageTokensCount response = client.beta().messages().countTokens(params); IO.println("Original tokens: " + response.contextManagement().get().originalInputTokens()); IO.println("After clearing: " + response.inputTokens()); IO.println("Savings: " + (response.contextManagement().get().originalInputTokens() - response.inputTokens()) + " tokens"); } ``` ```php PHP $client = new Client(); $response = $client->beta->messages->countTokens( messages: [ ['role' => 'user', 'content' => 'Continue our conversation...'] ], model: 'claude-opus-5', betas: ['context-management-2025-06-27'], contextManagement: [ 'edits' => [ [ 'type' => 'clear_tool_uses_20250919', 'trigger' => [ 'type' => 'input_tokens', 'value' => 30000 ], 'keep' => [ 'type' => 'tool_uses', 'value' => 5 ] ] ] ], ); echo "Original tokens: " . $response->contextManagement->originalInputTokens . "\n"; echo "After clearing: " . $response->inputTokens . "\n"; echo "Savings: " . ($response->contextManagement->originalInputTokens - $response->inputTokens) . " tokens\n"; ``` ```ruby Ruby client = Anthropic::Client.new response = client.beta.messages.count_tokens( model: "claude-opus-5", messages: [ { role: "user", content: "Continue our conversation..." } ], betas: ["context-management-2025-06-27"], context_management: { edits: [ { type: "clear_tool_uses_20250919", trigger: { type: "input_tokens", value: 30000 }, keep: { type: "tool_uses", value: 5 } } ] } ) puts "Original tokens: #{response.context_management.original_input_tokens}" puts "After clearing: #{response.input_tokens}" puts "Savings: #{response.context_management.original_input_tokens - response.input_tokens} tokens" ``` ```json Output { "input_tokens": 25000, "context_management": { "original_input_tokens": 70000 } } ``` The response shows both the final token count after context management is applied (`input_tokens`) and the original token count before any clearing occurred (`original_input_tokens`). ## Using with the memory tool Context editing can be combined with the [memory tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/memory-tool). When your conversation context approaches the configured clearing threshold, Claude receives an automatic warning to preserve important information. This enables Claude to save tool results or context to its memory files before they're cleared from the conversation history. This combination allows you to: * **Preserve important context:** Claude can write essential information from tool results to memory files before those results are cleared * **Maintain long-running workflows:** Enable agentic workflows that would otherwise exceed context limits by offloading information to persistent storage * **Access information on demand:** Claude can look up previously cleared information from memory files when needed, rather than keeping everything in the active context window For example, in a file editing workflow where Claude performs many operations, Claude can summarize completed changes to memory files as the context grows. When tool results are cleared, Claude retains access to that information through its memory system and can continue working effectively. To use both features together, enable them in your API request: ```bash cURL curl https://api.anthropic.com/v1/messages \ --header "x-api-key: $ANTHROPIC_API_KEY" \ --header "anthropic-version: 2023-06-01" \ --header "content-type: application/json" \ --header "anthropic-beta: context-management-2025-06-27" \ --data '{ "model": "claude-opus-5", "max_tokens": 4096, "messages": [ { "role": "user", "content": "Hello" } ], "tools": [ { "type": "memory_20250818", "name": "memory" } ], "context_management": { "edits": [ {"type": "clear_tool_uses_20250919"} ] } }' ``` ```bash CLI ant beta:messages create --beta context-management-2025-06-27 <<'YAML' model: claude-opus-5 max_tokens: 4096 messages: - role: user content: Hello tools: - type: memory_20250818 name: memory context_management: edits: - type: clear_tool_uses_20250919 YAML ``` ```python Python response = client.beta.messages.create( model="claude-opus-5", max_tokens=4096, messages=[{"role": "user", "content": "Hello"}], tools=[{"type": "memory_20250818", "name": "memory"}], betas=["context-management-2025-06-27"], context_management={"edits": [{"type": "clear_tool_uses_20250919"}]}, ) ``` ```typescript TypeScript const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }); const response = await anthropic.beta.messages.create({ model: "claude-opus-5", max_tokens: 4096, messages: [{ role: "user", content: "Hello" }], tools: [ { type: "memory_20250818", name: "memory" } ], betas: ["context-management-2025-06-27"], context_management: { edits: [{ type: "clear_tool_uses_20250919" }] } }); ``` ```csharp C# using Anthropic; using Anthropic.Models.Beta; using Anthropic.Models.Beta.Messages; using Messages = Anthropic.Models.Messages; AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Messages::Model.ClaudeOpus5, MaxTokens = 4096, Messages = [ new() { Role = Role.User, Content = "Hello" } ], Tools = [ new BetaMemoryTool20250818() ], Betas = [AnthropicBeta.ContextManagement2025_06_27], ContextManagement = new BetaContextManagementConfig { Edits = [new BetaClearToolUses20250919Edit()] } }; var response = await client.Beta.Messages.Create(parameters); Console.WriteLine(response); ``` ```go Go client := anthropic.NewClient() response, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 4096, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Hello")), }, Tools: []anthropic.BetaToolUnionParam{ {OfMemoryTool20250818: &anthropic.BetaMemoryTool20250818Param{}}, }, Betas: []anthropic.AnthropicBeta{anthropic.AnthropicBetaContextManagement2025_06_27}, ContextManagement: anthropic.BetaContextManagementConfigParam{ Edits: []anthropic.BetaContextManagementConfigEditUnionParam{ {OfClearToolUses20250919: &anthropic.BetaClearToolUses20250919EditParam{}}, }, }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java import com.anthropic.models.beta.messages.BetaMemoryTool20250818; import com.anthropic.models.beta.messages.BetaContextManagementConfig; import com.anthropic.models.beta.messages.BetaClearToolUses20250919Edit; import com.anthropic.models.beta.AnthropicBeta; // ... void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(4096L) .addUserMessage("Hello") .addTool(BetaMemoryTool20250818.builder().build()) .addBeta(AnthropicBeta.CONTEXT_MANAGEMENT_2025_06_27) .contextManagement(BetaContextManagementConfig.builder() .addEdit(BetaClearToolUses20250919Edit.builder().build()) .build()) .build(); BetaMessage response = client.beta().messages().create(params); IO.println(response); } ``` ```php PHP $client = new Client(); $response = $client->beta->messages->create( maxTokens: 4096, messages: [ ['role' => 'user', 'content' => 'Hello'] ], model: 'claude-opus-5', betas: ['context-management-2025-06-27'], tools: [ [ 'type' => 'memory_20250818', 'name' => 'memory' ] ], contextManagement: [ 'edits' => [ ['type' => 'clear_tool_uses_20250919'] ] ], ); echo $response; ``` ```ruby Ruby client = Anthropic::Client.new response = client.beta.messages.create( model: "claude-opus-5", max_tokens: 4096, messages: [{ role: "user", content: "Hello" }], tools: [ { type: "memory_20250818", name: "memory" } ], betas: ["context-management-2025-06-27"], context_management: { edits: [ { type: "clear_tool_uses_20250919" } ] } ) puts response ``` For the full memory tool reference including commands and examples, see [Memory tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/memory-tool). ## Client-side compaction (SDK) **Anthropic recommends server-side compaction over SDK compaction.** [Server-side compaction](https://platform.claude.com/docs/en/build-with-claude/compaction) handles context management automatically with less integration complexity, better token usage calculation, and no client-side limitations. Use SDK compaction only if you specifically need client-side control over the summarization process. The `compaction_control` parameter is deprecated in the Python, TypeScript, and Ruby SDKs and will be removed in a future version. The SDKs emit a deprecation warning when it is enabled. To use server-side compaction with a tool runner, pass the `compact_20260112` edit in the request's `context_management` parameter. Compaction is available in the [Python, TypeScript, and Ruby SDKs](https://platform.claude.com/docs/en/cli-sdks-libraries/overview) when using the [`tool_runner` method](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-runner). Compaction is an SDK feature that automatically manages conversation context by generating summaries when token usage grows too large. Unlike server-side context editing strategies that clear content, compaction instructs Claude to summarize the conversation history, then replaces the full history with that summary. This allows Claude to continue working on long-running tasks that would otherwise exceed the [context window](https://platform.claude.com/docs/en/build-with-claude/context-windows). ### How compaction works When compaction is enabled, the SDK monitors token usage after each model response: 1. **Threshold check:** The SDK calculates total tokens as `input_tokens + cache_creation_input_tokens + cache_read_input_tokens + output_tokens` (see [Prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) for the cache token fields). 2. **Summary generation:** When the threshold is exceeded, a summary prompt is injected as a user turn, and Claude generates a structured summary wrapped in `` tags. 3. **Context replacement:** The SDK extracts the summary and replaces the entire message history with it. 4. **Continuation:** The conversation resumes from the summary, with Claude picking up where it left off. ### Using compaction Add `compaction_control` to your `tool_runner` call to enable automatic summarization when token usage exceeds the threshold. Compaction runs client-side in the SDK `tool_runner` helpers, so it has no direct HTTP equivalent. Use [server-side compaction](https://platform.claude.com/docs/en/build-with-claude/compaction) instead, which handles compaction on Anthropic's servers. The CLI does not include a `tool_runner` helper. Use [server-side compaction](https://platform.claude.com/docs/en/build-with-claude/compaction) instead, which handles compaction on Anthropic's servers without SDK-side integration. ```python Python client = anthropic.Anthropic() runner = client.beta.messages.tool_runner( model="claude-opus-5", max_tokens=1024, tools=[read_file], messages=[{"role": "user", "content": "What's in config.json?"}], compaction_control={"enabled": True, "context_token_threshold": 100000}, ) for message in runner: print(f"Tokens used: {message.usage.input_tokens}") ``` ```typescript TypeScript const client = new Anthropic(); const runner = client.beta.messages.toolRunner({ model: "claude-opus-5", max_tokens: 1024, tools: [readFile], messages: [{ role: "user", content: "What's in config.json?" }], compactionControl: { enabled: true, contextTokenThreshold: 100000 } }); for await (const message of runner) { console.log(`Tokens used: ${message.usage.input_tokens}`); } ``` The C# SDK includes a tool runner, but it does not support client-side `compaction_control`. Use [server-side compaction](https://platform.claude.com/docs/en/build-with-claude/compaction) instead: it works with the tool runner by passing the `compact_20260112` edit in the request's `context_management` parameter. The Go SDK includes a tool runner, but it does not support client-side `compaction_control`. Use [server-side compaction](https://platform.claude.com/docs/en/build-with-claude/compaction) instead: it works with the tool runner by passing the `compact_20260112` edit in the request's `context_management` parameter. The Java SDK includes a tool runner, but it does not support client-side `compaction_control`. Use [server-side compaction](https://platform.claude.com/docs/en/build-with-claude/compaction) instead: it works with the tool runner by passing the `compact_20260112` edit in the request's `context_management` parameter. The PHP SDK includes a tool runner, but it does not support client-side `compaction_control`. Use [server-side compaction](https://platform.claude.com/docs/en/build-with-claude/compaction) instead: it works with the tool runner by passing the `compact_20260112` edit in the request's `context_management` parameter. ```ruby Ruby client = Anthropic::Client.new runner = client.beta.messages.tool_runner( model: "claude-opus-5", max_tokens: 1024, tools: [ReadFile.new], messages: [{ role: "user", content: "What's in config.json?" }], compaction_control: { enabled: true, context_token_threshold: 100000 } ) runner.each_message do |message| puts "Tokens used: #{message.usage.input_tokens}" end ``` #### What occurs during compaction As the conversation grows, the message history accumulates: **Before compaction (approaching 100k tokens):** ```json [ { "role": "user", "content": "Analyze all files and write a report..." }, { "role": "assistant", "content": "I'll help. Let me start by reading..." }, { "role": "user", "content": [{ "type": "tool_result", "tool_use_id": "...", "content": "..." }] }, { "role": "assistant", "content": "Based on file1.txt, I see..." }, { "role": "user", "content": [{ "type": "tool_result", "tool_use_id": "...", "content": "..." }] }, { "role": "assistant", "content": "After analyzing file2.txt..." } // ... 50 more exchanges like this ... ] ``` When tokens exceed the threshold, the SDK injects a summary request and Claude generates a summary. The entire history is then replaced: **After compaction (back to \~2–3k tokens):** ```json [ { "role": "assistant", "content": "# Task Overview\nThe user requested analysis of directory files to produce a summary report...\n\n# Current State\nAnalyzed 52 files across 3 subdirectories. Key findings documented in report.md...\n\n# Important Discoveries\n- Configuration files use YAML format\n- Found 3 deprecated dependencies\n- Test coverage at 67%\n\n# Next Steps\n1. Analyze remaining files in /src/legacy\n2. Complete final report sections...\n\n# Context to Preserve\nUser prefers markdown format with executive summary first..." } ] ``` Claude continues working from this summary as if it were the original conversation history. ### Configuration options | Parameter | Type | Required | Default | Description | | ------------------------- | ------- | -------- | -------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | | `enabled` | boolean | Yes | - | Whether to enable automatic compaction | | `context_token_threshold` | number | No | 100,000 | Token count at which compaction triggers | | `model` | string | No | Same as main model | Model to use for generating summaries | | `summary_prompt` | string | No | See [Default summary prompt](https://platform.claude.com/docs/en/build-with-claude/context-editing#default-summary-prompt) | Custom prompt for summary generation | #### Choosing a token threshold The threshold determines when compaction occurs. A lower threshold means more frequent compactions with smaller context windows. A higher threshold allows more context but risks hitting limits. Compaction runs client-side in the SDK `tool_runner` helpers, so it has no direct HTTP equivalent. Use [server-side compaction](https://platform.claude.com/docs/en/build-with-claude/compaction) instead, which handles compaction on Anthropic's servers. The CLI does not include a `tool_runner` helper. Use [server-side compaction](https://platform.claude.com/docs/en/build-with-claude/compaction) instead, which handles compaction on Anthropic's servers without SDK-side integration. ```python Python client = anthropic.Anthropic() runner = client.beta.messages.tool_runner( model="claude-opus-5", max_tokens=1024, tools=[read_file], messages=[{"role": "user", "content": "What's in config.json?"}], # Lower values compact more often; raise to 150000 when the task needs more context compaction_control={"enabled": True, "context_token_threshold": 50000}, ) for message in runner: print(f"Tokens used: {message.usage.input_tokens}") ``` ```typescript TypeScript const client = new Anthropic(); const runner = client.beta.messages.toolRunner({ model: "claude-opus-5", max_tokens: 1024, tools: [readFile], messages: [{ role: "user", content: "What's in config.json?" }], // Lower values compact more often; raise to 150000 when the task needs more context compactionControl: { enabled: true, contextTokenThreshold: 50000 } }); for await (const message of runner) { console.log(`Tokens used: ${message.usage.input_tokens}`); } ``` The C# SDK includes a tool runner, but it does not support client-side `compaction_control`. Use [server-side compaction](https://platform.claude.com/docs/en/build-with-claude/compaction) instead: it works with the tool runner by passing the `compact_20260112` edit in the request's `context_management` parameter. The Go SDK includes a tool runner, but it does not support client-side `compaction_control`. Use [server-side compaction](https://platform.claude.com/docs/en/build-with-claude/compaction) instead: it works with the tool runner by passing the `compact_20260112` edit in the request's `context_management` parameter. The Java SDK includes a tool runner, but it does not support client-side `compaction_control`. Use [server-side compaction](https://platform.claude.com/docs/en/build-with-claude/compaction) instead: it works with the tool runner by passing the `compact_20260112` edit in the request's `context_management` parameter. The PHP SDK includes a tool runner, but it does not support client-side `compaction_control`. Use [server-side compaction](https://platform.claude.com/docs/en/build-with-claude/compaction) instead: it works with the tool runner by passing the `compact_20260112` edit in the request's `context_management` parameter. ```ruby Ruby client = Anthropic::Client.new runner = client.beta.messages.tool_runner( model: "claude-opus-5", max_tokens: 1024, tools: [ReadFile.new], messages: [{ role: "user", content: "What's in config.json?" }], # Lower values compact more often; raise to 150000 when the task needs more context compaction_control: { enabled: true, context_token_threshold: 50000 } ) runner.each_message do |message| puts "Tokens used: #{message.usage.input_tokens}" end ``` #### Using a different model for summaries You can use a faster or cheaper model for generating summaries: Compaction runs client-side in the SDK `tool_runner` helpers, so it has no direct HTTP equivalent. Use [server-side compaction](https://platform.claude.com/docs/en/build-with-claude/compaction) instead, which handles compaction on Anthropic's servers. The CLI does not include a `tool_runner` helper. Use [server-side compaction](https://platform.claude.com/docs/en/build-with-claude/compaction) instead, which handles compaction on Anthropic's servers without SDK-side integration. ```python Python client = anthropic.Anthropic() runner = client.beta.messages.tool_runner( model="claude-opus-5", max_tokens=1024, tools=[read_file], messages=[{"role": "user", "content": "What's in config.json?"}], compaction_control={ "enabled": True, "context_token_threshold": 100000, "model": "claude-haiku-4-5", }, ) for message in runner: print(f"Tokens used: {message.usage.input_tokens}") ``` ```typescript TypeScript const client = new Anthropic(); const runner = client.beta.messages.toolRunner({ model: "claude-opus-5", max_tokens: 1024, tools: [readFile], messages: [{ role: "user", content: "What's in config.json?" }], compactionControl: { enabled: true, contextTokenThreshold: 100000, model: "claude-haiku-4-5" } }); for await (const message of runner) { console.log(`Tokens used: ${message.usage.input_tokens}`); } ``` The C# SDK includes a tool runner, but it does not support client-side `compaction_control`. Use [server-side compaction](https://platform.claude.com/docs/en/build-with-claude/compaction) instead: it works with the tool runner by passing the `compact_20260112` edit in the request's `context_management` parameter. The Go SDK includes a tool runner, but it does not support client-side `compaction_control`. Use [server-side compaction](https://platform.claude.com/docs/en/build-with-claude/compaction) instead: it works with the tool runner by passing the `compact_20260112` edit in the request's `context_management` parameter. The Java SDK includes a tool runner, but it does not support client-side `compaction_control`. Use [server-side compaction](https://platform.claude.com/docs/en/build-with-claude/compaction) instead: it works with the tool runner by passing the `compact_20260112` edit in the request's `context_management` parameter. The PHP SDK includes a tool runner, but it does not support client-side `compaction_control`. Use [server-side compaction](https://platform.claude.com/docs/en/build-with-claude/compaction) instead: it works with the tool runner by passing the `compact_20260112` edit in the request's `context_management` parameter. ```ruby Ruby client = Anthropic::Client.new runner = client.beta.messages.tool_runner( model: "claude-opus-5", max_tokens: 1024, tools: [ReadFile.new], messages: [{ role: "user", content: "What's in config.json?" }], compaction_control: { enabled: true, context_token_threshold: 100000, model: "claude-haiku-4-5" } ) runner.each_message do |message| puts "Tokens used: #{message.usage.input_tokens}" end ``` #### Custom summary prompts You can provide a custom prompt for domain-specific needs. Your prompt should instruct Claude to wrap its summary in `` tags. Compaction runs client-side in the SDK `tool_runner` helpers, so it has no direct HTTP equivalent. Use [server-side compaction](https://platform.claude.com/docs/en/build-with-claude/compaction) instead, which handles compaction on Anthropic's servers. The CLI does not include a `tool_runner` helper. Use [server-side compaction](https://platform.claude.com/docs/en/build-with-claude/compaction) instead, which handles compaction on Anthropic's servers without SDK-side integration. ```python Python client = anthropic.Anthropic() runner = client.beta.messages.tool_runner( model="claude-opus-5", max_tokens=1024, tools=[read_file], messages=[{"role": "user", "content": "What's in config.json?"}], compaction_control={ "enabled": True, "context_token_threshold": 100000, "summary_prompt": """Summarize the research conducted so far, including: - Sources consulted and key findings - Questions answered and remaining unknowns - Recommended next steps Wrap your summary in tags.""", }, ) for message in runner: print(f"Tokens used: {message.usage.input_tokens}") ``` ```typescript TypeScript const client = new Anthropic(); const runner = client.beta.messages.toolRunner({ model: "claude-opus-5", max_tokens: 1024, tools: [readFile], messages: [{ role: "user", content: "What's in config.json?" }], compactionControl: { enabled: true, contextTokenThreshold: 100000, summaryPrompt: `Summarize the research conducted so far, including: - Sources consulted and key findings - Questions answered and remaining unknowns - Recommended next steps Wrap your summary in tags.` } }); for await (const message of runner) { console.log(`Tokens used: ${message.usage.input_tokens}`); } ``` The C# SDK includes a tool runner, but it does not support client-side `compaction_control`. Use [server-side compaction](https://platform.claude.com/docs/en/build-with-claude/compaction) instead: it works with the tool runner by passing the `compact_20260112` edit in the request's `context_management` parameter. The Go SDK includes a tool runner, but it does not support client-side `compaction_control`. Use [server-side compaction](https://platform.claude.com/docs/en/build-with-claude/compaction) instead: it works with the tool runner by passing the `compact_20260112` edit in the request's `context_management` parameter. The Java SDK includes a tool runner, but it does not support client-side `compaction_control`. Use [server-side compaction](https://platform.claude.com/docs/en/build-with-claude/compaction) instead: it works with the tool runner by passing the `compact_20260112` edit in the request's `context_management` parameter. The PHP SDK includes a tool runner, but it does not support client-side `compaction_control`. Use [server-side compaction](https://platform.claude.com/docs/en/build-with-claude/compaction) instead: it works with the tool runner by passing the `compact_20260112` edit in the request's `context_management` parameter. ```ruby Ruby client = Anthropic::Client.new runner = client.beta.messages.tool_runner( model: "claude-opus-5", max_tokens: 1024, tools: [ReadFile.new], messages: [{ role: "user", content: "What's in config.json?" }], compaction_control: { enabled: true, context_token_threshold: 100000, summary_prompt: <<~PROMPT Summarize the research conducted so far, including: - Sources consulted and key findings - Questions answered and remaining unknowns - Recommended next steps Wrap your summary in tags. PROMPT } ) runner.each_message do |message| puts "Tokens used: #{message.usage.input_tokens}" end ``` ### Default summary prompt The built-in summary prompt instructs Claude to create a structured continuation summary including: 1. **Task Overview:** The user's core request, success criteria, and constraints. 2. **Current State:** What has been completed, files modified, and artifacts produced. 3. **Important Discoveries:** Technical constraints, decisions made, errors resolved, and failed approaches. 4. **Next Steps:** Specific actions needed, blockers, and priority order. 5. **Context to Preserve:** User preferences, domain-specific details, and commitments made. This structure enables Claude to resume work efficiently without losing important context or repeating mistakes. ```text wrap You have been working on the task described above but have not yet completed it. Write a continuation summary that will allow you (or another instance of yourself) to resume work efficiently in a future context window where the conversation history will be replaced with this summary. Your summary should be structured, concise, and actionable. Include: 1. Task Overview The user's core request and success criteria Any clarifications or constraints they specified 2. Current State What has been completed so far Files created, modified, or analyzed (with paths if relevant) Key outputs or artifacts produced 3. Important Discoveries Technical constraints or requirements uncovered Decisions made and their rationale Errors encountered and how they were resolved What approaches were tried that didn't work (and why) 4. Next Steps Specific actions needed to complete the task Any blockers or open questions to resolve Priority order if multiple steps remain 5. Context to Preserve User preferences or style requirements Domain-specific details that aren't obvious Any promises made to the user Be concise but complete—err on the side of including information that would prevent duplicate work or repeated mistakes. Write in a way that enables immediate resumption of the task. Wrap your summary in tags. ``` ### Limitations #### Server-side tools Compaction requires special consideration when using server-side tools such as [web search](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-search-tool) or [web fetch](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-fetch-tool). When using server-side tools, the SDK may incorrectly calculate token usage, causing compaction to trigger at the wrong time. For example, after a web search operation, the API response might show: ```json Output { "usage": { "input_tokens": 63000, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 270000, "output_tokens": 1400 } } ``` The SDK calculates total usage as 63,000 + 0 + 270,000 + 1,400 = 334,400 tokens. However, the `cache_read_input_tokens` value includes accumulated reads from multiple internal API calls made by the server-side tool, not your actual conversation context. Your real context length might only be the 63,000 `input_tokens`, but the SDK sees 334k and triggers compaction prematurely. **Workarounds:** * Use the [token counting](https://platform.claude.com/docs/en/build-with-claude/token-counting) endpoint to get accurate context length * Avoid compaction when using server-side tools extensively #### Tool use edge cases When the SDK triggers compaction while a tool use response is pending, it removes the tool use block from the message history before generating the summary. Claude will re-issue the tool call after resuming from the summary if still needed. ### Monitoring compaction Understanding when compaction triggers helps you tune thresholds and verify expected behavior. Compaction runs client-side in the SDK `tool_runner` helpers, so it has no direct HTTP equivalent. Use [server-side compaction](https://platform.claude.com/docs/en/build-with-claude/compaction) instead, which handles compaction on Anthropic's servers. The CLI does not include a `tool_runner` helper. Use [server-side compaction](https://platform.claude.com/docs/en/build-with-claude/compaction) instead, which handles compaction on Anthropic's servers without SDK-side integration. The Python SDK logs compaction events at the INFO level. Enable the `anthropic.lib.tools` logger: ```python Python import logging logging.basicConfig(level=logging.INFO) logging.getLogger("anthropic.lib.tools").setLevel(logging.INFO) # Logs will show: # INFO: Token usage 105000 has exceeded the threshold of 100000. Performing compaction. # INFO: Compaction complete. New token usage: 2500 ``` The TypeScript SDK's `toolRunner` supports compaction but does not log events. Detect compaction by watching `runner.params.messages.length` shrink between turns: ```typescript TypeScript let prevMsgCount = 0; for await (const message of runner) { const currMsgCount = runner.params.messages.length; if (currMsgCount < prevMsgCount) { console.log(`Compaction occurred: ${prevMsgCount} -> ${currMsgCount} messages`); console.log(`Input tokens after compaction: ${message.usage.input_tokens}`); } prevMsgCount = currMsgCount; } ``` The C# SDK's tool runner does not support `compaction_control`. Use [server-side compaction](https://platform.claude.com/docs/en/build-with-claude/compaction) instead. The Go SDK's tool runner does not support `compaction_control`. Use [server-side compaction](https://platform.claude.com/docs/en/build-with-claude/compaction) instead. The Java SDK's tool runner does not support `compaction_control`. Use [server-side compaction](https://platform.claude.com/docs/en/build-with-claude/compaction) instead. The PHP SDK's tool runner does not support `compaction_control`. Use [server-side compaction](https://platform.claude.com/docs/en/build-with-claude/compaction) instead. The Ruby SDK supports an `on_compact:` callback that fires when compaction occurs. Add it to your `compaction_control` configuration: ```ruby Ruby client = Anthropic::Client.new runner = client.beta.messages.tool_runner( model: "claude-opus-5", max_tokens: 1024, tools: [ReadFile.new], messages: [{ role: "user", content: "What's in config.json?" }], compaction_control: { enabled: true, context_token_threshold: 100000, on_compact: ->(tokens_before, tokens_after) do puts "Compaction occurred: #{tokens_before} -> #{tokens_after} tokens" end } ) runner.each_message do |message| puts "Tokens: #{message.usage.input_tokens}" end ``` ### When to use compaction **Good use cases:** * Long-running agent tasks that process many files or data sources * Research workflows that accumulate large amounts of information * Multistep tasks with clear, measurable progress * Tasks that produce artifacts (files, reports) that persist outside the conversation **Less ideal use cases:** * Tasks requiring precise recall of early conversation details * Workflows using server-side tools extensively * Tasks that need to maintain exact state across many variables ## Next steps Manage long conversations with server-side compaction, the recommended strategy for most use cases. Reduce cost and latency by caching prompt prefixes, and learn how context editing interacts with the cache. --- title: Context windows url: https://platform.claude.com/docs/en/build-with-claude/context-windows description: Understand how the context window works, how extended thinking and tool use count toward it, and how to manage context as conversations grow. --- As conversations grow, you'll eventually approach context window limits. For long-running conversations and agentic workflows, [server-side compaction](https://platform.claude.com/docs/en/build-with-claude/compaction) is the primary strategy for context management. ## How the context window works The "context window" refers to all the text a language model can reference when generating a response, including the response itself. This is different from the large corpus of data the language model was trained on, and instead represents a "working memory" for the model. A larger context window allows the model to handle more complex and lengthy prompts, but more context isn't automatically better. As token count grows, accuracy and recall degrade, a phenomenon known as *context rot*. This makes curating what's in context just as important as how much space is available. For more on why long contexts degrade and how to engineer around it, see [Effective context engineering](https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents). The following diagram illustrates the standard context window behavior for API requests1: ![Diagram of turns accumulating in the context window until the conversation approaches the token limit](https://platform.claude.com/docs/images/context-window.svg) *1 Chat interfaces such as [claude.ai](https://claude.ai/) can also manage the context window on a rolling "first in, first out" basis.* * **Progressive token accumulation:** As the conversation advances through turns, each user message and assistant response accumulates within the context window, and previous turns are preserved completely. * **Context window capacity:** The context window ([up to 1M tokens, depending on the model](https://platform.claude.com/docs/en/build-with-claude/context-windows#context-window-sizes-by-model)) holds the conversation history plus the new output Claude generates. * **Input-output flow:** Each turn consists of: * **Input phase:** Contains all previous conversation history plus the current user message * **Output phase:** Generates a text response that becomes part of the input for the next turn Everything in the request counts toward the context window: the system prompt, every message in `messages` (including tool results, images, and documents), and your tool definitions. The output Claude generates for the turn, including its extended thinking, counts too. Every response reports what the request consumed in its `usage` field. If you use [prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching), the input count is split across `input_tokens`, `cache_read_input_tokens`, and `cache_creation_input_tokens`, and all three count toward the window. To estimate a request before you send it, use the [token counting API](https://platform.claude.com/docs/en/build-with-claude/token-counting). ## Context window sizes by model Claude Opus 5, Claude Opus 4.8, Claude Opus 4.7, Claude Opus 4.6, Claude Sonnet 5, and Claude Sonnet 4.6 have a 1M-token context window on the Claude API, Amazon Bedrock, Google Cloud, and Microsoft Foundry. [Claude Mythos Preview](https://anthropic.com/glasswing) also has a 1M-token context window. Claude Fable 5 and Claude Mythos 5 (claude-fable-5 and claude-mythos-5) also have a 1M-token context window. A single request to any model with a 1M-token context window can generate up to 128k output tokens (`max_tokens`). Other Claude models, including Claude Sonnet 4.5, have a 200k-token context window. For every model with a 1M-token context window, 1M is the default: you don't need a beta header, and long-context requests are billed at [standard pricing](https://platform.claude.com/docs/en/about-claude/pricing#long-context-pricing). A single request can include up to 600 images or PDF pages (100 for models with a 200k-token context window). If you send many images or large documents, you might reach [request size limits](https://platform.claude.com/docs/en/api/overview#request-size-limits) before the token limit. See the [model comparison](https://platform.claude.com/docs/en/about-claude/models/overview#latest-models-comparison) table for a list of context window sizes by model. ## The context window with thinking With [thinking](https://platform.claude.com/docs/en/build-with-claude/thinking), all input and output tokens, including thinking tokens, count toward the context window limit, with a few nuances in multi-turn situations. Thinking tokens are a subset of your `max_tokens` parameter, are billed as output tokens, and count toward rate limits. With [adaptive thinking](https://platform.claude.com/docs/en/build-with-claude/thinking), Claude determines its thinking allocation dynamically, so thinking token usage varies from request to request. Whether thinking blocks from previous assistant turns stay in the context window depends on the model. On Claude Opus 4.5 and later Opus models, Claude Sonnet 4.6 and later Sonnet models, Claude Fable 5, Claude Mythos 5, and Claude Mythos Preview, the API keeps previous thinking blocks by default, and they count toward the context window like any other input tokens. On earlier Opus and Sonnet models and all Haiku models, the API automatically strips previous thinking blocks from the conversation history when you pass them back, which preserves token capacity for conversation content. For the per-model defaults, see [thinking block preservation by model](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-block-preservation-by-model). To override the default in either direction, use [thinking block clearing](https://platform.claude.com/docs/en/build-with-claude/context-editing#thinking-block-clearing). The following diagram shows how tokens are managed when thinking is enabled on a model that strips previous thinking blocks: ![Diagram of thinking on a model that strips previous thinking blocks: each turn's thinking block is generated in the output and not carried into later turns' input](https://platform.claude.com/docs/images/context-window-thinking.svg) * **Stripping thinking blocks:** On models that strip previous thinking blocks, thinking blocks (shown in dark gray) are generated during each turn's output phase but are not carried forward as input tokens for subsequent turns. You do not need to strip the thinking blocks yourself: if you pass them back, the Claude API strips them automatically. * **Billing:** Thinking tokens are billed as output tokens once, when they are generated. On models that keep previous thinking blocks, the kept blocks are then part of later requests' input and are billed as input tokens, like the rest of the conversation history. You can read more about the context window and thinking in the [Thinking](https://platform.claude.com/docs/en/build-with-claude/thinking) guide. ## The context window with thinking and tool use The following diagram illustrates how tokens are managed when you combine thinking with tool use on a model that strips previous thinking blocks: ![Diagram of thinking with tool use: thinking is kept with its tool result, then dropped on the next user turn on models that strip previous thinking blocks](https://platform.claude.com/docs/images/context-window-thinking-tools.svg) * **Input components:** Tools configuration and user message * **Output components:** Thinking + text response + tool use request * **Token calculation:** All input and output components count toward the context window, and all output components are billed as output tokens. * **Input components:** Every block in the first turn and the `tool_result`. You must return the thinking block with the corresponding tool results. This is the only case where you have to return thinking blocks. * **Output components:** After tool results have been passed back to Claude, Claude responds with only text (no additional thinking until the next `user` message, unless [interleaved thinking](https://platform.claude.com/docs/en/build-with-claude/thinking#interleaved-thinking) is enabled). * **Token calculation:** All input and output components count toward the context window, and all output components are billed as output tokens. * **Input components:** All inputs and the output from the previous turn are carried forward. The thinking block from the completed tool use cycle no longer has to stay in context: on models that strip previous thinking blocks, the API drops it automatically when you pass it back, and on models that keep previous thinking blocks, you can strip it yourself at this stage. This is also where you add the next `user` turn. * **Output components:** Because there is a new `user` turn outside the tool use cycle, Claude generates a new thinking block and continues from there. * **Token calculation:** On models that strip previous thinking blocks, the previous thinking tokens no longer count toward the context window. All other previous blocks still count toward the context window, as does the thinking block in the current `assistant` turn. * **Considerations for tool use with thinking:** * When you post tool results, you must include the entire unmodified thinking block that accompanies that tool request, including its signature. * The API uses cryptographic signatures to verify thinking block authenticity. If you modify a thinking block, the API returns an error. Most current Claude models support [interleaved thinking](https://platform.claude.com/docs/en/build-with-claude/thinking#interleaved-thinking), which lets Claude think between tool calls, including after it receives tool results. It is automatic on models with adaptive thinking; Claude Opus 4.5, Claude Sonnet 4.5, and earlier Claude 4 models require the `interleaved-thinking-2025-05-14` beta header, and Claude Haiku 4.5 does not support it. For more information about using tools with thinking, see [Thinking with tool use](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-with-tool-use). To reduce the context consumed by the tool definitions themselves, see [Manage tool context](https://platform.claude.com/docs/en/agents-and-tools/tool-use/manage-tool-context), or defer tool definitions with the [tool search tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool). ## Context awareness Claude Sonnet 5, Claude Sonnet 4.6, Claude Sonnet 4.5, and Claude Haiku 4.5 have **context awareness:** these models track their remaining context window (their "token budget") throughout a conversation. This lets the model manage long-running tasks against the space that remains rather than guess how many tokens are left. Context awareness is automatic: there is nothing for you to enable, and you never send the tags shown in this section yourself. The API injects them. ### How it works In the system prompt of every request, the API gives Claude its total context window: ```xml 200000 ``` The budget matches the context window available to your request: 1M tokens for Claude Sonnet 5 and Claude Sonnet 4.6, and 200k tokens for Claude Sonnet 4.5 and Claude Haiku 4.5. The examples in this section show a model with a 200k-token context window. After each tool call, the API gives Claude an update on its remaining capacity: ```xml Token usage: 35000/200000; 165000 remaining ``` Image tokens are included in these budgets. Claude Opus 4.7 and later Opus models, Claude Fable 5, and Claude Mythos 5 don't receive these injected tags. On Claude Opus 4.7 and later Opus models, Claude Fable 5, and Claude Mythos 5, you can give the model an explicit budget with [task budgets](https://platform.claude.com/docs/en/build-with-claude/task-budgets), which are in beta. For agents that span multiple sessions, design your state artifacts so that context recovery is fast when a new session starts. The [memory tool's multisession pattern](https://platform.claude.com/docs/en/agents-and-tools/tool-use/memory-tool#multisession-software-development-pattern) walks through a concrete approach. See also [Effective harnesses for long-running agents](https://www.anthropic.com/engineering/effective-harnesses-for-long-running-agents). For prompting guidance on using context awareness, see [Prompting best practices](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-prompting-best-practices#context-awareness-and-multiwindow-workflows). ## Manage context with compaction If your conversations regularly approach context window limits, use [server-side compaction](https://platform.claude.com/docs/en/build-with-claude/compaction). Compaction automatically summarizes earlier parts of the conversation on the server, so the conversation can continue past the context window limit. It is available in beta for Claude 4.6 and later models and [Claude Mythos Preview](https://anthropic.com/glasswing). For more specialized needs, [context editing](https://platform.claude.com/docs/en/build-with-claude/context-editing) offers additional strategies: * **Tool result clearing:** Clear old tool results in agentic workflows * **Thinking block clearing:** Manage thinking blocks when you use extended thinking Cached prompt prefixes still occupy the context window: [prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) changes what you pay for those tokens, not whether they count. ## Context window overflow behavior If the input alone already exceeds the model's context window, the API returns a 400 `invalid_request_error` ("prompt is too long") on every model. On Claude 4.5 models and newer, if input tokens plus `max_tokens` exceeds the context window size, the API accepts the request. If generation then reaches the context window limit, it stops with `stop_reason: "model_context_window_exceeded"`. On earlier models, the API returns a [validation error](https://platform.claude.com/docs/en/api/errors) instead. To opt in to the `model_context_window_exceeded` behavior on those models, use the `model-context-window-exceeded-2025-08-26` beta header. See [Stop reasons and fallback](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons) for details. To stay within context window limits, use the [token counting API](https://platform.claude.com/docs/en/build-with-claude/token-counting) to estimate token usage before sending messages to Claude. ## Next steps Server-side context compaction for managing long conversations that approach context window limits. Automatically manage conversation context as it grows with context editing. See the model comparison table for a list of context window sizes and input/output token pricing by model. Give Claude enhanced reasoning for complex tasks and control how thinking content is returned. --- title: Mid-conversation system messages and tool changes url: https://platform.claude.com/docs/en/build-with-claude/mid-conversation-system-messages description: Change system instructions or tool availability partway through a conversation without invalidating the cached prefix that came before them. --- For how zero data retention (ZDR) applies to this feature, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention). System instructions normally live in the top-level `system` field, ahead of every message in the conversation. That position is great for [prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching): the system prompt is part of the stable prefix, so subsequent turns hit the cache. It is a poor position for instructions you only discover you need partway through a session, because editing the top-level `system` field changes the very beginning of the prompt and invalidates the cache for everything that follows. Mid-conversation system messages close that gap. You append a `{"role": "system"}` message at the point in the conversation where the new instruction becomes relevant, instead of editing the top-level `system` field. The cached prefix stays the same, so the next request still reads it from cache, and the new instruction is still applied as a system instruction rather than as ordinary user text. This page covers two features: mid-conversation system messages, which are generally available, and [mid-conversation tool changes](https://platform.claude.com/docs/en/build-with-claude/mid-conversation-system-messages#mid-conversation-tool-changes), a beta introduced with Claude Opus 5 that applies the same approach to the `tools` array. Mid-conversation system messages are available on the Claude API, [Claude in Amazon Bedrock](https://platform.claude.com/docs/en/build-with-claude/claude-in-amazon-bedrock), and [Google Cloud](https://platform.claude.com/docs/en/build-with-claude/claude-on-vertex-ai). This feature is available on Claude Fable 5, [Claude Mythos 5](https://anthropic.com/glasswing), Claude Opus 4.8, Claude Opus 5, and Claude Sonnet 5. No beta header is required for mid-conversation system messages. Mid-conversation tool changes are in beta and require the `mid-conversation-tool-changes-2026-07-01` beta header. They are available on Claude Fable 5, Claude Mythos 5, Claude Opus 4.8, and Claude Opus 5, on the Claude API, Amazon Bedrock, and Google Cloud. They are not available on Claude Sonnet 5. ## Mid-conversation tool changes The `tools` array sits even earlier in the hashed request prefix than the top-level `system` field, so editing it invalidates the [prompt cache](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) for the entire conversation. Mid-conversation tool changes, a beta introduced with Claude Opus 5, are the tools counterpart to mid-conversation system messages. Instead of fixing the tool list for the lifetime of the conversation, you change which tools are offered to the model between turns: declare the full tool set in `tools` up front, then use `tool_addition` and `tool_removal` blocks to offer a tool to the model, or withdraw it, from a specific point in the conversation onward. The `tools` array itself never changes, so the cached prefix stays intact. `tool_addition` and `tool_removal` are content blocks in the `content` array of a `role: "system"` message, and they can be mixed with `text` blocks in the same message. The message follows the same placement rules as any mid-conversation system message (see [Limitations](https://platform.claude.com/docs/en/build-with-claude/mid-conversation-system-messages#limitations)), and the change applies from that point in the conversation onward. Each block's `tool` field references a tool rather than defining one: `{"type": "tool_reference", "name": "..."}` names a tool declared in the request's `tools` array, and [MCP connector](https://platform.claude.com/docs/en/agents-and-tools/mcp-connector) tools can be referenced individually with `mcp_tool_reference` (`server_name` and `name`) or as a whole toolset with `mcp_toolset_reference` (`server_name`). Referencing a name that is not declared in `tools` returns a 400 error. Every tool declared in `tools` is offered to the model from the start of the conversation unless it is declared with `defer_loading: true`, which keeps it withheld until a `tool_addition` block surfaces it. `tool_addition` also re-offers a tool that an earlier `tool_removal` withdrew. ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: mid-conversation-tool-changes-2026-07-01" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "tools": [ { "name": "get_weather", "description": "Get the current weather for a location.", "input_schema": { "type": "object", "properties": { "location": {"type": "string", "description": "City name"} }, "required": ["location"] } } ], "messages": [ { "role": "user", "content": "Say OK." }, { "role": "system", "content": [ { "type": "tool_removal", "tool": {"type": "tool_reference", "name": "get_weather"} } ] } ] }' ``` ```bash CLI ant beta:messages create --beta mid-conversation-tool-changes-2026-07-01 \ --transform 'content.#(type=="text").text' --raw-output <<'YAML' model: claude-opus-5 max_tokens: 1024 tools: - name: get_weather description: Get the current weather for a location. input_schema: type: object properties: location: type: string description: City name required: - location messages: - role: user content: Say OK. - role: system content: - type: tool_removal tool: type: tool_reference name: get_weather YAML ``` ```python Python client = anthropic.Anthropic() response = client.beta.messages.create( model="claude-opus-5", max_tokens=1024, betas=["mid-conversation-tool-changes-2026-07-01"], # The full tool set is declared up front and never changes, so the # cached prefix stays intact. tools=[ { "name": "get_weather", "description": "Get the current weather for a location.", "input_schema": { "type": "object", "properties": { "location": {"type": "string", "description": "City name"}, }, "required": ["location"], }, }, ], messages=[ { "role": "user", "content": "Say OK.", }, # Withdraw get_weather from this point onward. The block references # the tool by name instead of editing `tools`, so earlier turns stay # byte-identical and the cache still hits. { "role": "system", "content": [ { "type": "tool_removal", "tool": {"type": "tool_reference", "name": "get_weather"}, }, ], }, ], ) for block in response.content: if block.type == "text": print(block.text) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.beta.messages.create({ model: "claude-opus-5", max_tokens: 1024, betas: ["mid-conversation-tool-changes-2026-07-01"], // The full tool set is declared up front and never changes, so the // cached prefix stays intact. tools: [ { name: "get_weather", description: "Get the current weather for a location.", input_schema: { type: "object", properties: { location: { type: "string", description: "City name" } }, required: ["location"] } } ], messages: [ { role: "user", content: "Say OK." }, // Withdraw get_weather from this point onward. The block references the // tool by name instead of editing `tools`, so earlier turns stay // byte-identical and the cache still hits. { role: "system", content: [ { type: "tool_removal", tool: { type: "tool_reference", name: "get_weather" } } ] } ] }); for (const block of response.content) { if (block.type === "text") { console.log(block.text); } } ``` ```csharp C# using Anthropic.Models.Beta.Messages; using Messages = Anthropic.Models.Messages; AnthropicClient client = new(); var response = await client.Beta.Messages.Create(new MessageCreateParams { Model = Messages::Model.ClaudeOpus5, MaxTokens = 1024, Betas = ["mid-conversation-tool-changes-2026-07-01"], // The full tool set is declared up front and never changes, so the // cached prefix stays intact. Tools = [ new BetaTool { Name = "get_weather", Description = "Get the current weather for a location.", InputSchema = new InputSchema { Properties = new Dictionary { ["location"] = JsonSerializer.SerializeToElement(new { type = "string", description = "City name" }), }, Required = ["location"], }, }, ], Messages = [ new() { Role = Role.User, Content = "Say OK." }, // Withdraw get_weather from this point onward. The block references // the tool by name instead of editing `Tools`, so earlier turns stay // byte-identical and the cache still hits. new() { Role = Role.System, Content = new( [ new BetaRequestToolRemovalBlock { Tool = new BetaToolChangeToolReference { Name = "get_weather" }, }, ]), }, ], }); foreach (var block in response.Content) { if (block.TryPickText(out var text)) { Console.WriteLine(text.Text); } } ``` ```go Go client := anthropic.NewClient() response, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Betas: []anthropic.AnthropicBeta{"mid-conversation-tool-changes-2026-07-01"}, // The full tool set is declared up front and never changes, so the // cached prefix stays intact. Tools: []anthropic.BetaToolUnionParam{ {OfTool: &anthropic.BetaToolParam{ Name: "get_weather", Description: anthropic.String("Get the current weather for a location."), InputSchema: anthropic.BetaToolInputSchemaParam{ Properties: map[string]any{ "location": map[string]any{ "type": "string", "description": "City name", }, }, Required: []string{"location"}, }, }}, }, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Say OK.")), // Withdraw get_weather from this point onward. The block references // the tool by name instead of editing Tools, so earlier turns stay // byte-identical and the cache still hits. { Role: anthropic.BetaMessageParamRoleSystem, Content: []anthropic.BetaContentBlockParamUnion{ anthropic.NewBetaToolRemovalBlock(anthropic.BetaToolChangeToolReferenceParam{ Name: "get_weather", }), }, }, }, }) if err != nil { log.Fatal(err) } for _, block := range response.Content { if textBlock, ok := block.AsAny().(anthropic.BetaTextBlock); ok { fmt.Println(textBlock.Text) } } ``` ```java Java import com.anthropic.models.beta.messages.BetaContentBlockParam; import com.anthropic.models.beta.messages.BetaMessage; import com.anthropic.models.beta.messages.BetaMessageParam; import com.anthropic.models.beta.messages.BetaRequestToolRemovalBlock; import com.anthropic.models.beta.messages.BetaTool; import com.anthropic.models.beta.messages.MessageCreateParams; // ... AnthropicClient client = AnthropicOkHttpClient.fromEnv(); // The full tool set is declared up front and never changes, so the // cached prefix stays intact. BetaTool weatherTool = BetaTool.builder() .name("get_weather") .description("Get the current weather for a location.") .inputSchema(BetaTool.InputSchema.builder() .properties(BetaTool.InputSchema.Properties.builder() .putAdditionalProperty("location", JsonValue.from(Map.of( "type", "string", "description", "City name"))) .build()) .addRequired("location") .build()) .build(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .addBeta("mid-conversation-tool-changes-2026-07-01") .addTool(weatherTool) .addUserMessage("Say OK.") // Withdraw get_weather from this point onward. The block references // the tool by name instead of editing `tools`, so earlier turns stay // byte-identical and the cache still hits. .addMessage(BetaMessageParam.builder() .role(BetaMessageParam.Role.SYSTEM) .contentOfBetaContentBlockParams(List.of( BetaContentBlockParam.ofToolRemoval(BetaRequestToolRemovalBlock.builder() .referenceTool("get_weather") .build()))) .build()) .build(); BetaMessage response = client.beta().messages().create(params); response.content().stream() .flatMap(block -> block.text().stream()) .forEach(textBlock -> IO.println(textBlock.text())); ``` ```php PHP $client = new Client(); $response = $client->beta->messages->create( model: 'claude-opus-5', maxTokens: 1024, betas: ['mid-conversation-tool-changes-2026-07-01'], // The full tool set is declared up front and never changes, so the // cached prefix stays intact. tools: [ [ 'name' => 'get_weather', 'description' => 'Get the current weather for a location.', 'input_schema' => [ 'type' => 'object', 'properties' => [ 'location' => [ 'type' => 'string', 'description' => 'City name', ], ], 'required' => ['location'], ], ], ], messages: [ ['role' => 'user', 'content' => 'Say OK.'], // Withdraw get_weather from this point onward. The block references // the tool by name instead of editing `tools`, so earlier turns stay // byte-identical and the cache still hits. [ 'role' => 'system', 'content' => [ [ 'type' => 'tool_removal', 'tool' => ['type' => 'tool_reference', 'name' => 'get_weather'], ], ], ], ], ); foreach ($response->content as $block) { if ($block->type === 'text') { echo $block->text, PHP_EOL; } } ``` ```ruby Ruby client = Anthropic::Client.new response = client.beta.messages.create( model: "claude-opus-5", max_tokens: 1024, betas: ["mid-conversation-tool-changes-2026-07-01"], # The full tool set is declared up front and never changes, so the # cached prefix stays intact. tools: [ { name: "get_weather", description: "Get the current weather for a location.", input_schema: { type: "object", properties: { location: { type: "string", description: "City name" } }, required: ["location"] } } ], messages: [ { role: "user", content: "Say OK." }, # Withdraw get_weather from this point onward. The block references # the tool by name instead of editing `tools`, so earlier turns stay # byte-identical and the cache still hits. { role: "system", content: [ { type: "tool_removal", tool: { type: "tool_reference", name: "get_weather" } } ] } ] ) response.content.each do |block| puts block.text if block.type == :text end ``` Mid-conversation tool changes are in beta. To use them, include the beta header `mid-conversation-tool-changes-2026-07-01` in your requests. They are available on Claude Fable 5, Claude Mythos 5, Claude Opus 4.8, and Claude Opus 5, on the Claude API, Amazon Bedrock, and Google Cloud. ## When to use a mid-conversation system message [Prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) hashes the request prefix in order: `tools`, then `system`, then `messages`. A cache hit requires the prefix to match a recent request exactly, byte for byte, up to the cache breakpoint. That ordering means the top-level `system` field sits near the very start of the hashed prefix. Any change to it, even appending a sentence, produces a different hash, and the request misses the cache for the system prompt and every cached message after it. Mid-conversation system messages let you add the instruction at the **end** of the message history instead. Everything before the new instruction is unchanged, so the existing cache entry still matches, and only the new message is processed as fresh input. A few situations where this matters: * **Mid-session policy or persona changes.** A long agentic session needs a new constraint ("from now on, write all SQL as parameterized queries") after dozens of cached turns. Adding it to the top-level `system` field would re-process the entire history. * **Per-turn context that must be authoritative.** You want to inject a freshness note, a session deadline, or a tool-availability change with system-level weight, and it changes too often to live in the cached prefix. * **State changes your application observes.** Your application notices something Claude should treat as an operator-level fact: files changed on disk, the user toggled an auto-approve setting, available tools changed, or the remaining token budget dropped below a threshold. * **User input that should not interrupt an agentic loop.** A user types a follow-up while Claude is still executing tools for the previous request. Relaying it as a system message after the next tool result lets Claude fold the new input into the work it is already doing, instead of treating it as a fresh request to switch to. See [Placement after tool results](https://platform.claude.com/docs/en/build-with-claude/mid-conversation-system-messages#placement-after-tool-results) below. * **Mode switches that grant standing permissions.** A session-level mode can use a mid-conversation system message to grant standing consent to an expensive capability, such as automatically launching multiagent workflows, with a short refresher every several turns and an exit notice when the mode is turned off. For a worked example, see [Build an orchestration mode](https://platform.claude.com/docs/en/build-with-claude/mid-conversation-effort-example). In all of these cases you could put the instruction in a regular `user` message, and Claude does follow instructions that arrive in user turns. The difference is priority: a `user` message is treated as coming from the end user, while a `system` message is treated as coming from you, the application operator. When the two conflict, system instructions take precedence, so use the `system` role for operator-level facts and constraints that should hold even if the end user asks for something different. A mid-conversation system message keeps that operator-level priority without paying the cache-miss cost of editing the top-level `system` field. ## How it works Add a message with `"role": "system"` to the `messages` array. Use a plain string or content blocks for `content`, the same as a `user` or `assistant` turn. The instruction applies from that point in the conversation onward. When instructions conflict, later system messages take precedence over earlier ones, and mid-conversation system messages take precedence over the top-level `system` field for the turns that follow them. You can still set the top-level `system` field for instructions that should apply to the entire conversation. Reserve mid-conversation system messages for instructions that only become relevant later, or that you want to add without invalidating the cached prefix. ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "cache_control": {"type": "ephemeral"}, "system": "You are a code review assistant. Be concise.", "messages": [ { "role": "user", "content": "Review process() in utils.py for performance issues." }, { "role": "assistant", "content": "The list comprehension is fine for small inputs. For large inputs, consider a generator to avoid materializing the full list." }, { "role": "user", "content": "Now review the calling code that invokes process()." }, { "role": "system", "content": "From now on, every suggestion must include explicit type annotations." } ] }' ``` ```bash CLI ant messages create --transform 'content.#(type=="text").text' --raw-output <<'YAML' model: claude-opus-5 max_tokens: 1024 cache_control: type: ephemeral system: You are a code review assistant. Be concise. messages: - role: user content: Review process() in utils.py for performance issues. - role: assistant content: >- The list comprehension is fine for small inputs. For large inputs, consider a generator to avoid materializing the full list. - role: user content: Now review the calling code that invokes process(). - role: system content: From now on, every suggestion must include explicit type annotations. YAML ``` ```python Python client = anthropic.Anthropic() response = client.messages.create( model="claude-opus-5", max_tokens=1024, # Automatic prompt caching: each request caches the conversation so far, # and the next request reads the unchanged prefix from cache. cache_control={"type": "ephemeral"}, system="You are a code review assistant. Be concise.", messages=[ { "role": "user", "content": "Review process() in utils.py for performance issues.", }, { "role": "assistant", "content": "The list comprehension is fine for small inputs. For large inputs, consider a generator to avoid materializing the full list.", }, { "role": "user", "content": "Now review the calling code that invokes process().", }, # The reviewer realizes mid-session that all suggestions must # also pass the team's strict typing policy. Appending the # instruction here keeps earlier turns byte-identical, so the # prefix cached by the previous request is still read from cache. { "role": "system", "content": "From now on, every suggestion must include explicit type annotations.", }, ], ) for block in response.content: if block.type == "text": print(block.text) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, // Automatic prompt caching: each request caches the conversation so far, // and the next request reads the unchanged prefix from cache. cache_control: { type: "ephemeral" }, system: "You are a code review assistant. Be concise.", messages: [ { role: "user", content: "Review process() in utils.py for performance issues." }, { role: "assistant", content: "The list comprehension is fine for small inputs. For large inputs, consider a generator to avoid materializing the full list." }, { role: "user", content: "Now review the calling code that invokes process()." }, // The reviewer realizes mid-session that all suggestions must also pass // the team's strict typing policy. Appending the instruction here keeps // earlier turns byte-identical, so the prefix cached by the previous // request is still read from cache. { role: "system", content: "From now on, every suggestion must include explicit type annotations." } ] }); const textBlock = response.content.find( (block): block is Anthropic.TextBlock => block.type === "text" ); console.log(textBlock?.text); ``` ```csharp C# AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, // Automatic prompt caching: each request caches the conversation so far, // and the next request reads the unchanged prefix from cache. CacheControl = new CacheControlEphemeral(), System = "You are a code review assistant. Be concise.", Messages = [ new() { Role = Role.User, Content = "Review process() in utils.py for performance issues." }, new() { Role = Role.Assistant, Content = "The list comprehension is fine for small inputs. For large inputs, consider a generator to avoid materializing the full list." }, new() { Role = Role.User, Content = "Now review the calling code that invokes process()." }, // The reviewer realizes mid-session that all suggestions must also pass // the team's strict typing policy. Appending the instruction here keeps // earlier turns byte-identical, so the prefix cached by the previous // request is still read from cache. new() { Role = Role.System, Content = "From now on, every suggestion must include explicit type annotations." } ] }; var response = await client.Messages.Create(parameters); Console.WriteLine(response); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, // Automatic prompt caching: each request caches the conversation so far, // and the next request reads the unchanged prefix from cache. CacheControl: anthropic.NewCacheControlEphemeralParam(), System: []anthropic.TextBlockParam{ {Text: "You are a code review assistant. Be concise."}, }, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Review process() in utils.py for performance issues.")), anthropic.NewAssistantMessage(anthropic.NewTextBlock("The list comprehension is fine for small inputs. For large inputs, consider a generator to avoid materializing the full list.")), anthropic.NewUserMessage(anthropic.NewTextBlock("Now review the calling code that invokes process().")), // The reviewer realizes mid-session that all suggestions must also // pass the team's strict typing policy. Appending the instruction // here keeps earlier turns byte-identical, so the prefix cached by // the previous request is still read from cache. { Role: anthropic.MessageParamRoleSystem, Content: []anthropic.ContentBlockParamUnion{ anthropic.NewTextBlock("From now on, every suggestion must include explicit type annotations."), }, }, }, }) if err != nil { log.Fatal(err) } for _, block := range response.Content { if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok { fmt.Println(textBlock.Text) } } ``` ```java Java import com.anthropic.models.messages.CacheControlEphemeral; // ... import com.anthropic.models.messages.MessageParam; // ... AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) // Automatic prompt caching: each request caches the conversation so far, // and the next request reads the unchanged prefix from cache. .cacheControl(CacheControlEphemeral.builder().build()) .system("You are a code review assistant. Be concise.") .addUserMessage("Review process() in utils.py for performance issues.") .addAssistantMessage("The list comprehension is fine for small inputs. For large inputs, consider a generator to avoid materializing the full list.") .addUserMessage("Now review the calling code that invokes process().") // The reviewer realizes mid-session that all suggestions must also pass // the team's strict typing policy. Appending the instruction here keeps // earlier turns byte-identical, so the prefix cached by the previous // request is still read from cache. .addMessage(MessageParam.builder() .role(MessageParam.Role.SYSTEM) .content("From now on, every suggestion must include explicit type annotations.") .build()) .build(); Message response = client.messages().create(params); response.content().stream() .flatMap(block -> block.text().stream()) .forEach(textBlock -> IO.println(textBlock.text())); ``` ```php PHP use Anthropic\Messages\CacheControlEphemeral; // ... $client = new Client(); $response = $client->messages->create( maxTokens: 1024, messages: [ ['role' => 'user', 'content' => 'Review process() in utils.py for performance issues.'], ['role' => 'assistant', 'content' => 'The list comprehension is fine for small inputs. For large inputs, consider a generator to avoid materializing the full list.'], ['role' => 'user', 'content' => 'Now review the calling code that invokes process().'], // The reviewer realizes mid-session that all suggestions must also pass // the team's strict typing policy. Appending the instruction here keeps // earlier turns byte-identical, so the prefix cached by the previous // request is still read from cache. ['role' => 'system', 'content' => 'From now on, every suggestion must include explicit type annotations.'] ], model: 'claude-opus-5', // Automatic prompt caching: each request caches the conversation so far, // and the next request reads the unchanged prefix from cache. cacheControl: CacheControlEphemeral::with(), system: 'You are a code review assistant. Be concise.', ); foreach ($response->content as $block) { if ($block->type === 'text') { echo $block->text, PHP_EOL; } } ``` ```ruby Ruby client = Anthropic::Client.new response = client.messages.create( model: "claude-opus-5", max_tokens: 1024, # Automatic prompt caching: each request caches the conversation so far, # and the next request reads the unchanged prefix from cache. cache_control: { type: "ephemeral" }, system: "You are a code review assistant. Be concise.", messages: [ { role: "user", content: "Review process() in utils.py for performance issues." }, { role: "assistant", content: "The list comprehension is fine for small inputs. For large inputs, consider a generator to avoid materializing the full list." }, { role: "user", content: "Now review the calling code that invokes process()." }, # The reviewer realizes mid-session that all suggestions must also pass # the team's strict typing policy. Appending the instruction here keeps # earlier turns byte-identical, so the prefix cached by the previous # request is still read from cache. { role: "system", content: "From now on, every suggestion must include explicit type annotations." } ] ) response.content.each do |block| puts block.text if block.type == :text end ``` This example enables [automatic caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#automatic-caching) with the top-level `cache_control` field. Prompt caching is opt-in: if a request has no `cache_control` field (automatic or an [explicit breakpoint](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#explicit-cache-breakpoints)), nothing is cached and every request pays the regular input token price for the full conversation. With caching enabled, appending the system message leaves the already-cached turns unchanged, so the request that carries the new instruction still reads them from cache instead of processing them again. Caching also requires the conversation to meet the [minimum cacheable prompt length](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#cache-limitations); an example as short as this one falls below it, so `cache_creation_input_tokens` and `cache_read_input_tokens` stay at 0 until the conversation grows. A mid-conversation system message must immediately follow a `user` turn (or an `assistant` turn ending in a server tool result), and must either be the last entry in `messages` or be immediately followed by an `assistant` turn. A `user` message that carries `tool_result` blocks counts: in an agentic loop you can place the system message right after the tool results, before Claude's next turn. Any other position, including between an `assistant` `tool_use` block and the `tool_result` that answers it, returns a 400 error. ### Placement after tool results In an [agentic loop](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview), the system message goes after the `user` message that delivers the tool results. This is also where your application can relay input that the user typed while Claude was working, so the new context is absorbed without restarting the turn: ```json [ { "role": "user", "content": "Run the test suite and fix any failures." }, { "role": "assistant", "content": [{ "type": "tool_use", "id": "toolu_01", "name": "run_tests", "input": {} }] }, { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_01", "content": "12 passed, 0 failed" } ] }, { "role": "system", "content": "The user sent the following message while you were working: also update the changelog before you finish." } ] ``` Phrase the system content as context rather than as a command that overrides the user. State the fact ("new input arrived from the user: X", "the remaining token budget is now Y") and let Claude act on it. Claude is trained to resist instructions that appear to work against the user, and that protection still applies to the system role, so language such as "ignore what the user said" is less effective than stating what changed. This pattern is for relaying input from the conversation's own end user. Do not use it to pass tool output, retrieved documents, or other third-party content; keep that content in `tool_result` blocks (see [Limitations](https://platform.claude.com/docs/en/build-with-claude/mid-conversation-system-messages#limitations)). ## Combining with prompt caching Mid-conversation system messages and [prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) are designed to be used together: * **Enable caching explicitly.** Caching only happens when the request includes `cache_control`, either the top-level [automatic caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#automatic-caching) field or an [explicit breakpoint](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#explicit-cache-breakpoints) on a content block. A mid-conversation system message does not create a cache entry on its own, and without caching enabled there are no savings to preserve. * **Cache the stable prefix as usual.** Place `cache_control` on the last block that stays the same across requests, whether that is the end of the top-level `system` field, the end of your tool definitions, or a stable point in the message history. * **Append the system message after the breakpoint.** Because it comes after the cached prefix, it does not change the prefix hash and the cache still hits. * **A mid-conversation system message is itself cacheable.** Once it is in the conversation, it becomes part of the stable history. On the next turn you can move your cache breakpoint past it (or rely on [automatic caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#automatic-caching) to do so) and the system message is read from cache like any other turn. Avoid editing or removing a mid-conversation system message that has already been sent. Like any other change to earlier messages, that invalidates the cache from that point forward. If the instruction needs to evolve, append a new system message rather than rewriting the old one. Consecutive system messages are accepted and treated as a single system section, which follows the same placement rule as a whole. ## Limitations * **Not for the first message.** A `system` message cannot be the first entry in `messages`. Use the top-level `system` field for instructions that apply from the very start. * **Placement is constrained.** A `system` message must immediately follow a `user` turn (including a `user` turn that carries `tool_result` blocks) or an `assistant` turn ending in a server tool result, and must precede an `assistant` turn or end the array. It cannot sit between a `tool_use` block and its `tool_result`. Placing it elsewhere returns a 400 error. * **Not a place for untrusted content.** Claude treats system content as operator instructions and follows it. Do not place text from outside the conversation, such as raw tool output, retrieved documents, or web content, directly in a system message; doing so gives that text operator-level authority. Keep that data in `tool_result` blocks and continue to follow [Mitigate jailbreaks and prompt injections](https://platform.claude.com/docs/en/test-and-evaluate/strengthen-guardrails/mitigate-jailbreaks). ## Related How caching works, where to place breakpoints, and how to read cache usage fields. Find out exactly where two requests diverged when a cache hit you expected does not happen. Message structure, multi-turn conversations, and the `system` field. Writing effective prompts and system instructions. How `tool_use` and `tool_result` blocks are structured in the `messages` array. --- title: Prompt caching url: https://platform.claude.com/docs/en/build-with-claude/prompt-caching description: Cache prompt prefixes with `cache_control` to cut costs and latency, using automatic caching or explicit breakpoints with 5-minute or 1-hour TTLs. --- Prompt caching optimizes your API usage by allowing resuming from specific prefixes in your prompts. This significantly reduces processing time and costs for repetitive tasks or prompts with consistent elements. For how zero data retention (ZDR) applies to this feature, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention). There are two ways to enable prompt caching: * **[Automatic caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#automatic-caching)**: Add a single `cache_control` field at the top level of your request. The system automatically applies the cache breakpoint to the last cacheable block and moves it forward as conversations grow. Best for multi-turn conversations where the growing message history should be cached automatically. * **[Explicit cache breakpoints](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#explicit-cache-breakpoints)**: Place `cache_control` directly on individual content blocks for fine-grained control over exactly what gets cached. The simplest way to start is with automatic caching: ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "cache_control": {"type": "ephemeral"}, "system": "You are an AI assistant tasked with analyzing literary works. Your goal is to provide insightful commentary on themes, characters, and writing style.", "messages": [ { "role": "user", "content": "Analyze the major themes in Pride and Prejudice." } ] }' ``` ```bash CLI ant messages create --transform usage <<'YAML' model: claude-opus-5 max_tokens: 1024 cache_control: type: ephemeral system: >- You are an AI assistant tasked with analyzing literary works. Your goal is to provide insightful commentary on themes, characters, and writing style. messages: - role: user content: Analyze the major themes in Pride and Prejudice. YAML ``` ```python Python client = anthropic.Anthropic() response = client.messages.create( model="claude-opus-5", max_tokens=1024, cache_control={"type": "ephemeral"}, system="You are an AI assistant tasked with analyzing literary works. Your goal is to provide insightful commentary on themes, characters, and writing style.", messages=[ { "role": "user", "content": "Analyze the major themes in 'Pride and Prejudice'.", } ], ) print(response.usage.model_dump_json()) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, cache_control: { type: "ephemeral" }, system: "You are an AI assistant tasked with analyzing literary works. Your goal is to provide insightful commentary on themes, characters, and writing style.", messages: [ { role: "user", content: "Analyze the major themes in 'Pride and Prejudice'." } ] }); console.log(response.usage); ``` ```csharp C# AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, CacheControl = new CacheControlEphemeral(), System = "You are an AI assistant tasked with analyzing literary works. Your goal is to provide insightful commentary on themes, characters, and writing style.", Messages = [ new() { Role = Role.User, Content = "Analyze the major themes in 'Pride and Prejudice'." } ] }; var message = await client.Messages.Create(parameters); Console.WriteLine(message.Usage); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, CacheControl: anthropic.NewCacheControlEphemeralParam(), System: []anthropic.TextBlockParam{ {Text: "You are an AI assistant tasked with analyzing literary works. Your goal is to provide insightful commentary on themes, characters, and writing style."}, }, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Analyze the major themes in 'Pride and Prejudice'.")), }, }) if err != nil { log.Fatal(err) } fmt.Println(response.Usage.RawJSON()) ``` ```java Java import com.anthropic.models.messages.CacheControlEphemeral; // ... public class PromptCachingExample { public static void main(String[] args) { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .cacheControl(CacheControlEphemeral.builder().build()) .system("You are an AI assistant tasked with analyzing literary works. Your goal is to provide insightful commentary on themes, characters, and writing style.") .addUserMessage("Analyze the major themes in 'Pride and Prejudice'.") .build(); Message message = client.messages().create(params); System.out.println(message.usage()); } } ``` ```php PHP use Anthropic\Messages\CacheControlEphemeral; // ... $client = new Client(); $response = $client->messages->create( maxTokens: 1024, messages: [ ['role' => 'user', 'content' => "Analyze the major themes in 'Pride and Prejudice'."] ], model: 'claude-opus-5', cacheControl: CacheControlEphemeral::with(), system: "You are an AI assistant tasked with analyzing literary works. Your goal is to provide insightful commentary on themes, characters, and writing style.", ); echo json_encode($response->usage); ``` ```ruby Ruby client = Anthropic::Client.new response = client.messages.create( model: "claude-opus-5", max_tokens: 1024, cache_control: {type: "ephemeral"}, system: "You are an AI assistant tasked with analyzing literary works. Your goal is to provide insightful commentary on themes, characters, and writing style.", messages: [ { role: "user", content: "Analyze the major themes in 'Pride and Prejudice'." } ] ) puts response.usage ``` With automatic caching, the system caches all content up to and including the last cacheable block. On subsequent requests with the same prefix, cached content is reused automatically. *** ## How prompt caching works When you send a request with prompt caching enabled: 1. The system checks if a prompt prefix, up to a specified cache breakpoint, is already cached from a recent query. 2. If found, it uses the cached version, reducing processing time and costs. 3. Otherwise, it processes the full prompt and caches the prefix once the response begins. This is especially useful for: * Prompts with many examples * Large amounts of context or background information * Repetitive tasks with consistent instructions * Long multi-turn conversations By default, the cache has a 5-minute lifetime. The cache is refreshed for no additional cost each time the cached content is used. The lifetime is measured from the start of the request that writes or reads the cache entry, not from the end of its response. Time spent generating a response counts against the lifetime: if a response takes 4 minutes to stream, a follow-up request that reuses the same cached prefix must start within about 1 minute of that response completing. If you find that 5 minutes is too short, Anthropic also offers a 1-hour cache duration [at additional cost](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#pricing). For more information, see [1-hour cache duration](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#1-hour-cache-duration). **Prompt caching caches the full prefix** Prompt caching references the entire prompt - `tools`, `system`, and `messages` (in that order) up to and including the block designated with `cache_control`. *** ## Pricing Prompt caching introduces a new pricing structure. The following table shows the price per million tokens for each supported model: | Model | Base Input Tokens | 5m Cache Writes | 1h Cache Writes | Cache Hits & Refreshes | Output Tokens | | ------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | --------------- | --------------- | ---------------------- | ------------- | | Claude Fable 5 | $10 / MTok | $12.50 / MTok | $20 / MTok | $1 / MTok | $50 / MTok | | Claude Mythos 5 ([limited availability](https://anthropic.com/glasswing)) | $10 / MTok | $12.50 / MTok | $20 / MTok | $1 / MTok | $50 / MTok | | Claude Opus 5 | $5 / MTok | $6.25 / MTok | $10 / MTok | $0.50 / MTok | $25 / MTok | | Claude Opus 4.8 | $5 / MTok | $6.25 / MTok | $10 / MTok | $0.50 / MTok | $25 / MTok | | Claude Opus 4.7 | $5 / MTok | $6.25 / MTok | $10 / MTok | $0.50 / MTok | $25 / MTok | | Claude Opus 4.6 | $5 / MTok | $6.25 / MTok | $10 / MTok | $0.50 / MTok | $25 / MTok | | Claude Opus 4.5 | $5 / MTok | $6.25 / MTok | $10 / MTok | $0.50 / MTok | $25 / MTok | | Claude Opus 4.1 ([retired, except on Bedrock and Google Cloud](https://platform.claude.com/docs/en/about-claude/model-deprecations)) | $15 / MTok | $18.75 / MTok | $30 / MTok | $1.50 / MTok | $75 / MTok | | Claude Opus 4 ([retired, except on Google Cloud](https://platform.claude.com/docs/en/about-claude/model-deprecations)) | $15 / MTok | $18.75 / MTok | $30 / MTok | $1.50 / MTok | $75 / MTok | | Claude Sonnet 5 | $2 / MTok | $2.50 / MTok | $4 / MTok | $0.20 / MTok | $10 / MTok | | Claude Sonnet 4.6 | $3 / MTok | $3.75 / MTok | $6 / MTok | $0.30 / MTok | $15 / MTok | | Claude Sonnet 4.5 | $3 / MTok | $3.75 / MTok | $6 / MTok | $0.30 / MTok | $15 / MTok | | Claude Sonnet 4 ([retired, except on Bedrock and Google Cloud](https://platform.claude.com/docs/en/about-claude/model-deprecations)) | $3 / MTok | $3.75 / MTok | $6 / MTok | $0.30 / MTok | $15 / MTok | | Claude Haiku 4.5 | $1 / MTok | $1.25 / MTok | $2 / MTok | $0.10 / MTok | $5 / MTok | | Claude Haiku 3.5 ([retired, except on Bedrock and Google Cloud](https://platform.claude.com/docs/en/about-claude/model-deprecations)) | $0.80 / MTok | $1 / MTok | $1.60 / MTok | $0.08 / MTok | $4 / MTok | The previous table reflects the following pricing multipliers for prompt caching: * 5-minute cache write tokens are 1.25 times the base input tokens price * 1-hour cache write tokens are 2 times the base input tokens price * Cache read tokens are 0.1 times the base input tokens price These multipliers stack with other pricing modifiers such as the Batch API discount and data residency. See [pricing](https://platform.claude.com/docs/en/about-claude/pricing) for full details. *** ## Supported models Prompt caching (both automatic and explicit) is supported on all [active Claude models](https://platform.claude.com/docs/en/about-claude/models/overview). *** ## Automatic caching Automatic caching is the simplest way to enable prompt caching. Instead of placing `cache_control` on individual content blocks, add a single `cache_control` field at the top level of your request body. The system automatically applies the cache breakpoint to the last cacheable block. ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "cache_control": {"type": "ephemeral"}, "system": "You are a helpful assistant that remembers our conversation.", "messages": [ {"role": "user", "content": "My name is Alex. I work on machine learning."}, {"role": "assistant", "content": "Nice to meet you, Alex! How can I help with your ML work today?"}, {"role": "user", "content": "What did I say I work on?"} ] }' ``` ```bash CLI ant messages create --transform usage <<'YAML' model: claude-opus-5 max_tokens: 1024 cache_control: type: ephemeral system: You are a helpful assistant that remembers our conversation. messages: - role: user content: My name is Alex. I work on machine learning. - role: assistant content: Nice to meet you, Alex! How can I help with your ML work today? - role: user content: What did I say I work on? YAML ``` ```python Python client = anthropic.Anthropic() response = client.messages.create( model="claude-opus-5", max_tokens=1024, cache_control={"type": "ephemeral"}, system="You are a helpful assistant that remembers our conversation.", messages=[ {"role": "user", "content": "My name is Alex. I work on machine learning."}, { "role": "assistant", "content": "Nice to meet you, Alex! How can I help with your ML work today?", }, {"role": "user", "content": "What did I say I work on?"}, ], ) print(response.usage.model_dump_json()) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, cache_control: { type: "ephemeral" }, system: "You are a helpful assistant that remembers our conversation.", messages: [ { role: "user", content: "My name is Alex. I work on machine learning." }, { role: "assistant", content: "Nice to meet you, Alex! How can I help with your ML work today?" }, { role: "user", content: "What did I say I work on?" } ] }); console.log(response.usage); ``` ```csharp C# AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, CacheControl = new CacheControlEphemeral(), System = "You are a helpful assistant that remembers our conversation.", Messages = [ new() { Role = Role.User, Content = "My name is Alex. I work on machine learning." }, new() { Role = Role.Assistant, Content = "Nice to meet you, Alex! How can I help with your ML work today?" }, new() { Role = Role.User, Content = "What did I say I work on?" } ] }; var message = await client.Messages.Create(parameters); Console.WriteLine(message.Usage); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, CacheControl: anthropic.NewCacheControlEphemeralParam(), System: []anthropic.TextBlockParam{ {Text: "You are a helpful assistant that remembers our conversation."}, }, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("My name is Alex. I work on machine learning.")), anthropic.NewAssistantMessage(anthropic.NewTextBlock("Nice to meet you, Alex! How can I help with your ML work today?")), anthropic.NewUserMessage(anthropic.NewTextBlock("What did I say I work on?")), }, }) if err != nil { log.Fatal(err) } fmt.Println(response.Usage.RawJSON()) ``` ```java Java import com.anthropic.models.messages.CacheControlEphemeral; // ... public class AutomaticCachingExample { public static void main(String[] args) { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .cacheControl(CacheControlEphemeral.builder().build()) .system("You are a helpful assistant that remembers our conversation.") .addUserMessage("My name is Alex. I work on machine learning.") .addAssistantMessage("Nice to meet you, Alex! How can I help with your ML work today?") .addUserMessage("What did I say I work on?") .build(); Message message = client.messages().create(params); System.out.println(message.usage()); } } ``` ```php PHP use Anthropic\Messages\CacheControlEphemeral; // ... $client = new Client(); $response = $client->messages->create( maxTokens: 1024, messages: [ ['role' => 'user', 'content' => 'My name is Alex. I work on machine learning.'], ['role' => 'assistant', 'content' => 'Nice to meet you, Alex! How can I help with your ML work today?'], ['role' => 'user', 'content' => 'What did I say I work on?'], ], model: 'claude-opus-5', cacheControl: CacheControlEphemeral::with(), system: 'You are a helpful assistant that remembers our conversation.', ); echo json_encode($response->usage); ``` ```ruby Ruby client = Anthropic::Client.new response = client.messages.create( model: "claude-opus-5", max_tokens: 1024, cache_control: {type: "ephemeral"}, system: "You are a helpful assistant that remembers our conversation.", messages: [ {role: "user", content: "My name is Alex. I work on machine learning."}, {role: "assistant", content: "Nice to meet you, Alex! How can I help with your ML work today?"}, {role: "user", content: "What did I say I work on?"} ] ) puts response.usage ``` ### How automatic caching works in multi-turn conversations With automatic caching, the cache point moves forward automatically as conversations grow. Each new request caches everything up to the last cacheable block, and previous content is read from cache. | Request | Content | Cache behavior | | --------- | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | | Request 1 | System + User(1) + Asst(1) + **User(2)** ◀ cache | Everything written to cache | | Request 2 | System + User(1) + Asst(1) + User(2) + Asst(2) + **User(3)** ◀ cache | System through User(2) read from cache; Asst(2) + User(3) written to cache | | Request 3 | System + User(1) + Asst(1) + User(2) + Asst(2) + User(3) + Asst(3) + **User(4)** ◀ cache | System through User(3) read from cache; Asst(3) + User(4) written to cache | The cache breakpoint automatically moves to the last cacheable block in each request, so you don't need to update any `cache_control` markers as the conversation grows. ### TTL support By default, automatic caching uses a 5-minute TTL. You can specify a 1-hour TTL at 2x the base input token price: ```json { "cache_control": { "type": "ephemeral", "ttl": "1h" } } ``` ### Combining with block-level caching Automatic caching is compatible with [explicit cache breakpoints](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#explicit-cache-breakpoints). When used together, the automatic cache breakpoint uses one of the 4 available breakpoint slots. This lets you combine both approaches. For example, use an explicit breakpoint to cache your system prompt, while automatic caching handles the conversation: ```json { "model": "claude-opus-5", "max_tokens": 1024, "cache_control": { "type": "ephemeral" }, "system": [ { "type": "text", "text": "You are a helpful assistant.", "cache_control": { "type": "ephemeral" } } ], "messages": [{ "role": "user", "content": "What are the key terms?" }] } ``` ### What stays the same Automatic caching uses the same underlying caching infrastructure. Pricing, minimum token thresholds, context ordering requirements, and the 20-block lookback window all apply the same as with explicit breakpoints. ### Edge cases * If the last block already has an explicit `cache_control` with the same TTL, automatic caching is a no-op. * If the last block has an explicit `cache_control` with a different TTL, the API returns a 400 error. * If 4 explicit block-level breakpoints already exist, the API returns a 400 error (no slots left for automatic caching). * If the last block is not eligible as an automatic cache breakpoint target, the system silently walks backwards to find the nearest eligible block. If none is found, caching is skipped. Automatic caching is available on every platform except the legacy [Amazon Bedrock (Opus 4.6 and earlier)](https://platform.claude.com/docs/en/build-with-claude/claude-on-amazon-bedrock-legacy) integration. On that integration, the API returns a 400 error for a top-level `cache_control` field, so use [explicit cache breakpoints](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#explicit-cache-breakpoints) instead. *** ## Explicit cache breakpoints For more control over caching, you can place `cache_control` directly on individual content blocks. This is useful when you need to cache different sections that change at different frequencies, or need fine-grained control over exactly what gets cached. ### Structuring your prompt Place static content (tool definitions, system instructions, context, examples) at the beginning of your prompt. Mark the end of the reusable content for caching using the `cache_control` parameter. Cache prefixes are created in the following order: `tools`, `system`, then `messages`. This order forms a hierarchy where each level builds upon the previous ones. #### How automatic prefix checking works You can use just one cache breakpoint at the end of your static content, and the system will automatically find the longest prefix that a prior request already wrote to the cache. Understanding how this works helps you optimize your caching strategy. **Three core principles:** 1. **Cache writes happen only at your breakpoint.** Marking a block with `cache_control` writes exactly one cache entry: a hash of the prefix ending at that block. The system does not write entries for any earlier position. Because the hash is cumulative, covering everything up to and including the breakpoint, changing any block at or before the breakpoint produces a different hash on the next request. 2. **Cache reads look backward for entries that prior requests wrote.** On each request the system computes the prefix hash at your breakpoint and checks for a matching cache entry. If none exists, it walks backward one block at a time, checking whether the prefix hash at each earlier position matches something already in the cache. It is looking for prior writes, not for stable content. 3. **The lookback window is 20 blocks.** The system checks at most 20 positions per breakpoint, counting the breakpoint itself as the first. If the system finds no matching entry in that window, checking stops (or resumes from the next explicit breakpoint, if any). **Example: Lookback in a growing conversation** You append new blocks each turn and set `cache_control` on the final block of each request: * **Turn 1:** 10 blocks, breakpoint on block 10. No prior cache entries exist. The system writes an entry at block 10. * **Turn 2:** 15 blocks, breakpoint on block 15. Block 15 has no entry, so the system walks back to block 10 and finds the turn-1 entry. Cache hit at block 10; the system processes only blocks 11 through 15 fresh and writes a new entry at block 15. * **Turn 3:** 35 blocks, breakpoint on block 35. The system checks 20 positions (blocks 35 through 16) and finds nothing. The turn-2 entry at block 15 is one position outside the window, so there is no cache hit. Adding a second breakpoint at block 15 starts a second lookback window there, which finds the turn-2 entry. **Common mistake: Breakpoint on content that changes every request** Your prompt has a large static system context (blocks 1 through 5) followed by a per-request block containing a timestamp and the user message (block 6). You set `cache_control` on block 6: * **Request 1:** Cache write at block 6. The hash includes the timestamp. * **Request 2:** The timestamp differs, so the prefix hash at block 6 differs. The lookback walks through blocks 5, 4, 3, 2, and 1, but the system never wrote an entry at any of those positions. No cache hit. You pay for a fresh cache write on every request and never get a read. The lookback does not find stable content behind your breakpoint and cache it. It finds entries that prior requests already wrote, and writes happen only at breakpoints. Move `cache_control` to block 5, the last block that stays the same across requests, and every subsequent request reads the cached prefix. [Automatic caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#automatic-caching) hits the same trap: it places the breakpoint on the last cacheable block, which in this structure is the one that changes every request, so use an explicit breakpoint on block 5 instead. **Key takeaway:** Place `cache_control` on the last block whose prefix is identical across the requests you want to share a cache. In a growing conversation the final block works as long as each turn adds fewer than 20 blocks: earlier content never changes, so the next request's lookback finds the prior write. For a prompt with a varying suffix (timestamps, per-request context, the incoming message), place the breakpoint at the end of the static prefix, not on the varying block. #### When to use multiple breakpoints You can define up to 4 cache breakpoints if you want to: * Cache different sections that change at different frequencies (for example, tools rarely change, but context updates daily) * Have more control over exactly what gets cached * Ensure a cache hit when a growing conversation pushes your breakpoint 20 or more blocks past the last cache write **Important limitation:** The lookback can only find entries that earlier requests already wrote. If a growing conversation pushes your breakpoint 20 or more blocks past the last write, the lookback window misses it. Add a second breakpoint closer to that position from the start so a write accumulates there before you need it. ### Understanding cache breakpoint costs **Cache breakpoints themselves don't add any cost.** You are only charged for: * **Cache writes:** When new content is written to the cache (25% more than base input tokens for 5-minute TTL) * **Cache reads:** When cached content is used (10% of base input token price) * **Regular input tokens:** For any uncached content Adding more `cache_control` breakpoints doesn't increase your costs - you still pay the same amount based on what content is actually cached and read. The breakpoints give you control over what sections can be cached independently. *** ## Caching strategies and considerations ### Cache limitations On the Claude API, [Claude Platform on AWS](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws), [Google Cloud](https://platform.claude.com/docs/en/build-with-claude/claude-on-vertex-ai), and [Microsoft Foundry](https://platform.claude.com/docs/en/build-with-claude/claude-in-microsoft-foundry), the minimum cacheable prompt length is: * 512 tokens for Claude Opus 5, Claude Fable 5, and [Claude Mythos 5](https://anthropic.com/glasswing) * 2,048 tokens for [Claude Mythos Preview](https://anthropic.com/glasswing) and Claude Opus 4.7 * 4,096 tokens for Claude Opus 4.6 and Claude Opus 4.5 * 1,024 tokens for Claude Opus 4.8, Claude Sonnet 5, Claude Sonnet 4.6, Claude Sonnet 4.5, Claude Opus 4.1 ([retired, except on Bedrock and Google Cloud](https://platform.claude.com/docs/en/about-claude/model-deprecations)), Claude Opus 4 ([retired, except on Google Cloud](https://platform.claude.com/docs/en/about-claude/model-deprecations)), and Claude Sonnet 4 ([retired, except on Bedrock and Google Cloud](https://platform.claude.com/docs/en/about-claude/model-deprecations)) * 4,096 tokens for Claude Haiku 4.5 * 2,048 tokens for Claude Haiku 3.5 ([retired, except on Bedrock and Google Cloud](https://platform.claude.com/docs/en/about-claude/model-deprecations)) These minimums apply on every platform where each model is available. Shorter prompts cannot be cached, even if marked with `cache_control`. Any requests to cache fewer than this number of tokens will be processed without caching, and no error is returned. To verify whether a prompt was cached, check the [response usage fields](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#tracking-cache-performance): if both `cache_creation_input_tokens` and `cache_read_input_tokens` are 0, the prompt was not cached (likely because it did not meet the minimum length requirement). If your prompt falls just short of the minimum for your model and platform, expanding the cached content to reach the threshold is often worthwhile. Cache reads cost significantly less than uncached input tokens, so reaching the minimum can reduce costs for frequently reused prompts. [Bedrock](https://platform.claude.com/docs/en/build-with-claude/claude-in-amazon-bedrock) is an AWS-operated platform. On Bedrock, see the [Bedrock prompt caching documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html) for the per-model minimums, failure behavior, and usage-field names that apply. For concurrent requests, note that a cache entry only becomes available after the first response begins. If you need cache hits for parallel requests, wait for the first response before sending subsequent requests. Currently, "ephemeral" is the only supported cache type, which by default has a 5-minute lifetime. ### What can be cached Most blocks in the request can be cached. This includes: * Tools: Tool definitions in the `tools` array * System messages: Content blocks in the `system` array * Text messages: Content blocks in the `messages.content` array, for both user and assistant turns * Images & Documents: Content blocks in the `messages.content` array, in user turns * Tool use and tool results: Content blocks in the `messages.content` array, in both user and assistant turns Each of these elements can be cached, either automatically or by marking them with `cache_control`. ### What cannot be cached While most request blocks can be cached, there are some exceptions: * Thinking blocks cannot be cached directly with `cache_control`. However, thinking blocks CAN be cached alongside other content when they appear in previous assistant turns. When cached this way, they DO count as input tokens when read from cache. * Sub-content blocks (like [citations](https://platform.claude.com/docs/en/build-with-claude/citations)) themselves cannot be cached directly. Instead, cache the top-level block. In the case of citations, the top-level document content blocks that serve as the source material for citations can be cached. This allows you to use prompt caching with citations effectively by caching the documents that citations will reference. * Empty text blocks cannot be cached. ### What invalidates the cache Modifications to cached content can invalidate some or all of the cache. As described in [Structuring your prompt](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#structuring-your-prompt), the cache follows the hierarchy: `tools` → `system` → `messages`. Changes at each level invalidate that level and all subsequent levels. The following table shows which parts of the cache are invalidated by different types of changes. ✘ indicates that the cache is invalidated, while ✓ indicates that the cache remains valid. | What changes | Tools cache | System cache | Messages cache | Impact | | --------------------------------------------------------- | -------------- | -------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Tool definitions** | ✘ | ✘ | ✘ | Modifying tool definitions (names, descriptions, parameters) invalidates the entire cache | | **Web search toggle** | ✓ | ✘ | ✘ | Enabling/disabling web search modifies the system prompt | | **Citations toggle** | ✓ | ✘ | ✘ | Enabling/disabling citations modifies the system prompt | | **Speed setting** | ✓ | ✘ | ✘ | Switching between [`speed: "fast"` and standard speed](https://platform.claude.com/docs/en/build-with-claude/fast-mode) invalidates system and message caches | | **Tool choice** | ✓ | ✓ | ✘ | Changes to `tool_choice` parameter only affect message blocks | | **Images** | ✓ | ✓ | ✘ | Adding/removing images anywhere in the prompt affects message blocks | | **Thinking parameters** | Model-specific | Model-specific | ✘ | The thinking configuration (mode, and `budget_tokens` in extended mode) is rendered into the prompt, so changing it always invalidates message blocks; tool and system caches are also invalidated on models that render the configuration ahead of them. See [Thinking and prompt caching](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-and-prompt-caching). | | **Effort setting** | Model-specific | Model-specific | ✘ | Changing the [`output_config.effort`](https://platform.claude.com/docs/en/build-with-claude/effort) value always invalidates message blocks, with the same model-specific effect on tool and system caches as thinking parameters. Setting effort explicitly to the model's default is equivalent to omitting it and does not invalidate. | | **Non-tool results passed to extended thinking requests** | ✓ | ✓ | Model-specific | On Opus 4.5+ and Sonnet 4.6+, thinking blocks are preserved by default, so the cache remains valid (✓). On earlier Opus/Sonnet models and all Haiku models, all previously-cached thinking blocks are stripped from context, and any messages that follow those thinking blocks are removed from the cache (✘). For more details, see [Caching with thinking blocks](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#caching-with-thinking-blocks). | On Claude Fable 5, [Claude Mythos 5](https://anthropic.com/glasswing), Claude Opus 4.8, Claude Opus 5, and Claude Sonnet 5, you can add a new system instruction partway through a conversation without invalidating the system or message caches. Append a `{"role": "system"}` message to `messages` instead of editing the top-level `system` field, so the cached prefix stays unchanged. See [Mid-conversation system messages](https://platform.claude.com/docs/en/build-with-claude/mid-conversation-system-messages). ### Tracking cache performance Monitor cache performance using these API response fields, within `usage` in the response (or `message_start` event if [streaming](https://platform.claude.com/docs/en/build-with-claude/streaming)): * `cache_creation_input_tokens`: Number of tokens written to the cache when creating a new entry. * `cache_read_input_tokens`: Number of tokens retrieved from the cache for this request. * `input_tokens`: Number of input tokens which were not read from or used to create a cache (that is, tokens after the last cache breakpoint). **Understanding the token breakdown** The `input_tokens` field represents only the tokens that come **after the last cache breakpoint** in your request - not all the input tokens you sent. To calculate total input tokens: ```text wrap total_input_tokens = cache_read_input_tokens + cache_creation_input_tokens + input_tokens ``` **Spatial explanation:** * `cache_read_input_tokens` = tokens before breakpoint already cached (reads) * `cache_creation_input_tokens` = tokens before breakpoint being cached now (writes) * `input_tokens` = tokens after your last breakpoint (not eligible for cache) **Example:** If you have a request with 100,000 tokens of cached content (read from cache), 0 tokens of new content being cached, and 50 tokens in your user message (after the cache breakpoint): * `cache_read_input_tokens`: 100,000 * `cache_creation_input_tokens`: 0 * `input_tokens`: 50 * **Total input tokens processed:** 100,050 tokens This is important for understanding both costs and rate limits, as `input_tokens` will typically be much smaller than your total input when using caching effectively. ### Caching with thinking blocks When using [thinking](https://platform.claude.com/docs/en/build-with-claude/thinking) with prompt caching, thinking blocks have special behavior: **Automatic caching alongside other content:** While thinking blocks cannot be explicitly marked with `cache_control`, they get cached as part of the request content when you make subsequent API calls with tool results. This commonly happens during tool use when you pass thinking blocks back to continue the conversation. **Input token counting:** When thinking blocks are read from cache, they count as input tokens in your usage metrics. This is important for cost calculation and token budgeting. **Cache invalidation patterns:** * Cache remains valid when only tool results are provided as user messages * On Opus 4.5+ and Sonnet 4.6+, thinking blocks are preserved by default even when non-tool-result user content is added, so the cache remains valid * On earlier Opus/Sonnet models and all Haiku models, cache gets invalidated when non-tool-result user content is added, causing all previous thinking blocks to be stripped from context * This caching behavior occurs even without explicit `cache_control` markers For more details on cache invalidation, see [What invalidates the cache](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#what-invalidates-the-cache). **Example with tool use:** ```text wrap Request 1: User: "What's the weather in Paris?" Response: [thinking_block_1] + [tool_use block 1] Request 2: User: ["What's the weather in Paris?"], Assistant: [thinking_block_1] + [tool_use block 1], User: [tool_result_1, cache=True] Response: [thinking_block_2] + [text block 2] # Request 2 caches its request content (not the response) # The cache includes: user message, thinking_block_1, tool_use block 1, and tool_result_1 Request 3: User: ["What's the weather in Paris?"], Assistant: [thinking_block_1] + [tool_use block 1], User: [tool_result_1, cache=True], Assistant: [thinking_block_2] + [text block 2], User: [Text response, cache=True] # On earlier Opus/Sonnet and all Haiku models, non-tool-result user block causes prior thinking blocks to be stripped; on Opus 4.5+/Sonnet 4.6+ they are kept ``` On earlier Opus/Sonnet models and all Haiku models, all previous thinking blocks are removed from context at this point. On Opus 4.5+ and Sonnet 4.6+, prior thinking blocks are kept by default and remain part of the cached prefix. For more detailed information, see [Thinking and prompt caching](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-and-prompt-caching). ### Cache storage and sharing Prompt caching uses [workspace](https://platform.claude.com/docs/en/manage-claude/workspaces)-level isolation. Caches are isolated per workspace, ensuring data separation between workspaces within the same organization. This applies to the Claude API, Claude Platform on AWS, and Microsoft Foundry; Bedrock and Google Cloud maintain organization-level cache isolation. If you use multiple workspaces, review your caching strategy to account for this difference. * **Organization and workspace isolation:** Caches are isolated between organizations. Different organizations never share caches, even if they use identical prompts. Caches are also isolated per workspace within an organization on the Claude API, Claude Platform on AWS, and Microsoft Foundry; Bedrock and Google Cloud use organization-level isolation only. * **Exact matching:** Cache hits require 100% identical prompt segments, including all text and images up to and including the block marked with cache control. * **Output token generation:** Prompt caching has no effect on output token generation. The response you receive is identical to what you would get if prompt caching were not used. ### Best practices for effective caching To optimize prompt caching performance: * Start with [automatic caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#automatic-caching) for multi-turn conversations. It handles breakpoint management automatically. * Use [explicit block-level breakpoints](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#explicit-cache-breakpoints) when you need to cache different sections with different change frequencies. * Cache stable, reusable content like system instructions, background information, large contexts, or frequent tool definitions. * Place cached content at the prompt's beginning for best performance. * Use cache breakpoints strategically to separate different cacheable prefix sections. * Place the breakpoint on the last block that stays identical across requests. For a prompt with a static prefix and a varying suffix (timestamps, per-request context, the incoming message), that is the end of the prefix, not the varying block. * Regularly analyze cache hit rates and adjust your strategy as needed. ### Optimizing for different use cases Tailor your prompt caching strategy to your scenario: * Conversational agents: Reduce cost and latency for extended conversations, especially those with long instructions or uploaded documents. * Coding assistants: Improve autocomplete and codebase Q\&A by keeping relevant sections or a summarized version of the codebase in the prompt. * Large document processing: Incorporate complete long-form material including images in your prompt without increasing response latency. * Detailed instruction sets: Share extensive lists of instructions, procedures, and examples to fine-tune Claude's responses. Developers often include an example or two in the prompt, but with prompt caching you can get even better performance by including 20+ diverse examples of high quality answers. * Agentic tool use: Enhance performance for scenarios involving multiple tool calls and iterative code changes, where each step typically requires a new API call. * Talk to books, papers, documentation, podcast transcripts, and other longform content: Bring any knowledge base alive by embedding the entire document(s) into the prompt, and letting users ask it questions. ### Troubleshooting common issues If experiencing unexpected behavior: [Cache diagnostics](https://platform.claude.com/docs/en/build-with-claude/cache-diagnostics) (beta) has the API compare consecutive requests and report exactly where the prompt prefix diverged, which automatically handles many of the steps in this list. * Ensure cached sections are identical across calls. For explicit breakpoints, verify that `cache_control` markers are in the same locations * Check that calls are made within the cache lifetime (5 minutes by default) * Verify that `tool_choice`, image usage, the thinking configuration, and `output_config.effort` remain consistent between calls * Validate that you are caching at least the minimum number of tokens for your model and platform (see [Cache limitations](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#cache-limitations)) * Confirm your breakpoint is on a block that stays identical across requests. Cache writes happen only at the breakpoint, and if that block changes (timestamps, per-request context, the incoming message), the prefix hash never matches. The lookback does not find stable content behind the breakpoint; it only finds entries that earlier requests wrote at their own breakpoints * Verify that the keys in your `tool_use` content blocks have stable ordering as some languages (for example, Swift, Go) randomize key order during JSON conversion, breaking caches * Use [cache diagnostics](https://platform.claude.com/docs/en/build-with-claude/cache-diagnostics) to have the API compare consecutive requests and report which part of the prompt diverged Changes to `tool_choice` or the presence/absence of images anywhere in the prompt will invalidate the cache, requiring a new cache entry to be created. For more details on cache invalidation, see [What invalidates the cache](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#what-invalidates-the-cache). *** ## 1-hour cache duration If you find that 5 minutes is too short, Anthropic also offers a 1-hour cache duration [at additional cost](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#pricing). The 1-hour cache duration is available on the Claude API, [Amazon Bedrock](https://platform.claude.com/docs/en/build-with-claude/claude-in-amazon-bedrock), [Amazon Bedrock (Opus 4.6 and earlier)](https://platform.claude.com/docs/en/build-with-claude/claude-on-amazon-bedrock-legacy), [Claude Platform on AWS](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws), [Google Cloud](https://platform.claude.com/docs/en/build-with-claude/claude-on-vertex-ai), and [Microsoft Foundry](https://platform.claude.com/docs/en/build-with-claude/claude-in-microsoft-foundry). To use the extended cache, include `ttl` in the `cache_control` definition like this: ```json "cache_control": { "type": "ephemeral", "ttl": "1h" } ``` The response includes detailed cache information like the following: ```json Output { "usage": { "input_tokens": 2048, "cache_read_input_tokens": 1800, "cache_creation_input_tokens": 248, "output_tokens": 503, "cache_creation": { "ephemeral_5m_input_tokens": 148, "ephemeral_1h_input_tokens": 100 } } } ``` Note that the current `cache_creation_input_tokens` field equals the sum of the values in the `cache_creation` object. If you see `ephemeral_5m_input_tokens` writes you didn't request while using server tools such as web search, see [Tool use with prompt caching](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-use-with-prompt-caching#server-tool-results-are-cached-automatically). ### When to use the 1-hour cache If you have prompts that are used at a regular cadence (that is, system prompts that are used more frequently than every 5 minutes), continue to use the 5-minute cache, because this will continue to be refreshed at no additional charge. The 1-hour cache is best used in the following scenarios: * When you have prompts that are likely used less frequently than 5 minutes, but more frequently than every hour. For example, when an agentic side-agent will take longer than 5 minutes, or when storing a long chat conversation with a user and you generally expect that user may not respond in the next 5 minutes. * When latency is important and your follow up prompts may be sent beyond 5 minutes. * When you want to improve your rate limit utilization, because cache hits are not deducted against your rate limit. The 5-minute and 1-hour cache behave the same with respect to latency. You will generally see improved time-to-first-token for long documents. ### Mixing different TTLs You can use both 1-hour and 5-minute cache controls in the same request, but with an important constraint: Cache entries with longer TTL must appear before shorter TTLs (that is, a 1-hour cache entry must appear before any 5-minute cache entries). When mixing TTLs, the API determines three billing locations in your prompt: 1. Position `A`: The token count at the highest cache hit (or 0 if no hits). 2. Position `B`: The token count at the highest 1-hour `cache_control` block after `A` (or equals `A` if none exist). 3. Position `C`: The token count at the last `cache_control` block. If `B` and/or `C` are larger than `A`, they will necessarily be cache misses, because `A` is the highest cache hit. You'll be charged for: 1. Cache read tokens for `A`. 2. 1-hour cache write tokens for `(B - A)`. 3. 5-minute cache write tokens for `(C - B)`. Here are three examples. This depicts the input tokens of 3 requests, each of which has different cache hits and cache misses. Each has a different calculated pricing, shown in the colored boxes, as a result. ![Mixing TTLs Diagram](https://platform.claude.com/docs/images/prompt-cache-mixed-ttl.svg) *** ## Pre-warming the cache Cache pre-warming lets you load your system prompt or tool definitions into the prompt cache before a user triggers a real request. This eliminates the cache-miss latency penalty on the first user interaction, reducing time-to-first-token (TTFT) for latency-sensitive applications. ### How it works Set `max_tokens: 0` in your request. The API reads your prompt into the model and writes the cache at any `cache_control` breakpoint, then returns immediately without generating any output. The response has an empty `content` array, `stop_reason: "max_tokens"`, and a fully populated `usage` block. Place the `cache_control` breakpoint on the last block that is shared with the follow-up request (typically your system prompt or tool definitions), not on the placeholder user message. Otherwise the cache entry is keyed to the placeholder and the follow-up request won't hit it. Use the same thinking configuration and `output_config.effort` as your follow-up requests too: those values are rendered into the prompt (see [What invalidates the cache](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#what-invalidates-the-cache)), so a pre-warm with a different configuration can write an entry your real traffic never hits. This means using an [explicit cache breakpoint](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#explicit-cache-breakpoints) rather than [automatic caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#automatic-caching), since automatic caching places the breakpoint on the last block, which here is the placeholder. The placeholder user message can be any string with non-whitespace content (the examples here use `"warmup"`); its content is read into the model but never answered. A pre-warm request incurs a **cache write** charge if the prefix is not already cached, the same as any other request. Check `usage.cache_creation_input_tokens` in the response to confirm a write occurred. Zero output tokens are billed. ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-opus-5", "max_tokens": 0, "system": [ { "type": "text", "text": "You are an expert software engineer with deep knowledge of distributed systems...", "cache_control": {"type": "ephemeral"} } ], "messages": [{"role": "user", "content": "warmup"}] }' ``` ```bash CLI ant messages create \ --transform '{stop_reason,content,usage}' --format yaml <<'YAML' model: claude-opus-5 max_tokens: 0 system: - type: text text: >- You are an expert software engineer with deep knowledge of distributed systems... cache_control: type: ephemeral messages: - role: user content: warmup YAML ``` ```python Python client = anthropic.Anthropic() # Fire this before users arrive to warm the shared system-prompt cache. prewarm = client.messages.create( model="claude-opus-5", max_tokens=0, system=[ { "type": "text", "text": "You are an expert software engineer with deep knowledge of distributed systems...", "cache_control": {"type": "ephemeral"}, } ], messages=[{"role": "user", "content": "warmup"}], ) print(prewarm.stop_reason) # "max_tokens" print(prewarm.content) # [] print(prewarm.usage) ``` ```typescript TypeScript const client = new Anthropic(); // Fire this before users arrive to warm the shared system-prompt cache. const prewarm = await client.messages.create({ model: "claude-opus-5", max_tokens: 0, system: [ { type: "text", text: "You are an expert software engineer with deep knowledge of distributed systems...", cache_control: { type: "ephemeral" } } ], messages: [{ role: "user", content: "warmup" }] }); console.log(prewarm.stop_reason); // "max_tokens" console.log(prewarm.content); // [] console.log(prewarm.usage); ``` ```csharp C# AnthropicClient client = new(); var prewarm = await client.Messages.Create( new() { Model = Model.ClaudeOpus5, MaxTokens = 0, System = new( [ new TextBlockParam { Text = "You are an expert software engineer with deep knowledge of distributed systems...", CacheControl = new(), }, ] ), Messages = [new() { Role = Role.User, Content = "warmup" }], } ); Console.WriteLine(prewarm.StopReason?.Raw()); // "max_tokens" Console.WriteLine(prewarm.Content.Count); // 0 Console.WriteLine(prewarm.Usage); ``` ```go Go client := anthropic.NewClient() prewarm, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 0, System: []anthropic.TextBlockParam{ { Text: "You are an expert software engineer with deep knowledge of distributed systems...", CacheControl: anthropic.NewCacheControlEphemeralParam(), }, }, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("warmup")), }, }) if err != nil { panic(err) } fmt.Println(prewarm.StopReason) // "max_tokens" fmt.Println(prewarm.Content) // [] fmt.Println(prewarm.Usage.RawJSON()) ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); Message prewarm = client.messages().create(MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(0) .systemOfTextBlockParams(List.of(TextBlockParam.builder() .text("You are an expert software engineer with deep knowledge of distributed systems...") .cacheControl(CacheControlEphemeral.builder().build()) .build())) .addUserMessage("warmup") .build()); IO.println(prewarm.stopReason()); // Optional[max_tokens] IO.println(prewarm.content()); // [] IO.println(prewarm.usage()); ``` ```php PHP $client = new Client(); $prewarm = $client->messages->create( model: Model::CLAUDE_OPUS_5, maxTokens: 0, system: [ [ 'type' => 'text', 'text' => 'You are an expert software engineer with deep knowledge of distributed systems...', 'cache_control' => ['type' => 'ephemeral'], ], ], messages: [['role' => 'user', 'content' => 'warmup']], ); echo $prewarm->stopReason->value, PHP_EOL; // "max_tokens" echo json_encode($prewarm->content), PHP_EOL; // [] echo json_encode($prewarm->usage), PHP_EOL; ``` ```ruby Ruby client = Anthropic::Client.new prewarm = client.messages.create( model: Anthropic::Model::CLAUDE_OPUS_5, max_tokens: 0, system_: [ { type: "text", text: "You are an expert software engineer with deep knowledge of distributed systems...", cache_control: {type: "ephemeral"} } ], messages: [{role: "user", content: "warmup"}] ) puts prewarm.stop_reason # :max_tokens puts prewarm.content # [] puts prewarm.usage ``` The API returns an empty `content` array: ```json Output { "id": "msg_01XFDUDYJgAACzvnptvVoYEL", "type": "message", "role": "assistant", "content": [], "model": "claude-opus-5", "stop_reason": "max_tokens", "stop_sequence": null, "usage": { "input_tokens": 8, "cache_creation_input_tokens": 5120, "cache_read_input_tokens": 0, "cache_creation": { "ephemeral_5m_input_tokens": 5120, "ephemeral_1h_input_tokens": 0 }, "iterations": [ { "input_tokens": 8, "output_tokens": 0, "cache_read_input_tokens": 0, "cache_creation_input_tokens": 5120, "cache_creation": { "ephemeral_5m_input_tokens": 5120, "ephemeral_1h_input_tokens": 0 }, "type": "message" } ], "output_tokens": 0, "service_tier": "standard", "inference_geo": "global" } } ``` ### Typical usage pattern Fire a pre-warm request when your application starts (or on a scheduled interval), then send real user requests after the pre-warm completes: ```bash cURL # Warm the cache at application startup or on a scheduled interval. curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-opus-5", "max_tokens": 0, "system": [ { "type": "text", "text": "You are an expert software engineer with deep knowledge of distributed systems...", "cache_control": {"type": "ephemeral"} } ], "messages": [{"role": "user", "content": "warmup"}] }' # Later, when the user submits a message, the system-prompt prefix is already cached. curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "system": [ { "type": "text", "text": "You are an expert software engineer with deep knowledge of distributed systems...", "cache_control": {"type": "ephemeral"} } ], "messages": [{"role": "user", "content": "How do I implement a binary search tree?"}] }' ``` ```bash CLI # Warm the cache at application startup or on a scheduled interval. ant messages create --transform usage <<'YAML' model: claude-opus-5 max_tokens: 0 system: - type: text text: >- You are an expert software engineer with deep knowledge of distributed systems... cache_control: type: ephemeral messages: - role: user content: warmup YAML # Later, when the user submits a message, the system-prompt prefix is already cached. ant messages create --transform 'content.#(type=="text").text' --raw-output <<'YAML' model: claude-opus-5 max_tokens: 1024 system: - type: text text: >- You are an expert software engineer with deep knowledge of distributed systems... cache_control: type: ephemeral messages: - role: user content: How do I implement a binary search tree? YAML ``` ```python Python client = anthropic.Anthropic() SYSTEM_PROMPT = [ { "type": "text", "text": "You are an expert software engineer with deep knowledge of distributed systems...", "cache_control": {"type": "ephemeral"}, } ] def prewarm_cache() -> None: """Call this at application startup or on a scheduled interval.""" client.messages.create( model="claude-opus-5", max_tokens=0, system=SYSTEM_PROMPT, messages=[{"role": "user", "content": "warmup"}], ) def respond(user_message: str) -> anthropic.types.Message: """The real user request; benefits from a warm cache.""" return client.messages.create( model="claude-opus-5", max_tokens=1024, system=SYSTEM_PROMPT, messages=[{"role": "user", "content": user_message}], ) # Warm the cache before any user traffic arrives. prewarm_cache() # Later, when the user submits a message, the system-prompt prefix is already cached. response = respond("How do I implement a binary search tree?") for block in response.content: if block.type == "text": print(block.text) ``` ```typescript TypeScript const client = new Anthropic(); const SYSTEM_PROMPT: Anthropic.TextBlockParam[] = [ { type: "text", text: "You are an expert software engineer with deep knowledge of distributed systems...", cache_control: { type: "ephemeral" } } ]; // Call this at application startup or on a scheduled interval. async function prewarmCache(): Promise { await client.messages.create({ model: "claude-opus-5", max_tokens: 0, system: SYSTEM_PROMPT, messages: [{ role: "user", content: "warmup" }] }); } // The real user request; benefits from a warm cache. async function respond(userMessage: string): Promise { return client.messages.create({ model: "claude-opus-5", max_tokens: 1024, system: SYSTEM_PROMPT, messages: [{ role: "user", content: userMessage }] }); } // Warm the cache before any user traffic arrives. await prewarmCache(); // Later, when the user submits a message, the system-prompt prefix is already cached. const response = await respond("How do I implement a binary search tree?"); const textBlock = response.content.find( (block): block is Anthropic.TextBlock => block.type === "text" ); console.log(textBlock?.text); ``` ```csharp C# AnthropicClient client = new(); List systemPrompt = [ new TextBlockParam { Text = "You are an expert software engineer with deep knowledge of distributed systems...", CacheControl = new(), }, ]; // Call this at application startup or on a scheduled interval. async Task PrewarmCache() => await client.Messages.Create( new() { Model = Model.ClaudeOpus5, MaxTokens = 0, System = new(systemPrompt), Messages = [new() { Role = Role.User, Content = "warmup" }], } ); // The real user request; benefits from a warm cache. async Task Respond(string userMessage) => await client.Messages.Create( new() { Model = Model.ClaudeOpus5, MaxTokens = 1024, System = new(systemPrompt), Messages = [new() { Role = Role.User, Content = userMessage }], } ); // Warm the cache before any user traffic arrives. await PrewarmCache(); // Later, when the user submits a message, the system-prompt prefix is already cached. var response = await Respond("How do I implement a binary search tree?"); foreach (var block in response.Content) { if (block.TryPickText(out var textBlock)) { Console.WriteLine(textBlock.Text); } } ``` ```go Go var client = anthropic.NewClient() var systemPrompt = []anthropic.TextBlockParam{ { Text: "You are an expert software engineer with deep knowledge of distributed systems...", CacheControl: anthropic.NewCacheControlEphemeralParam(), }, } // Call this at application startup or on a scheduled interval. func prewarmCache() error { _, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 0, System: systemPrompt, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("warmup")), }, }) return err } // The real user request; benefits from a warm cache. func respond(userMessage string) (*anthropic.Message, error) { return client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, System: systemPrompt, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock(userMessage)), }, }) } func main() { // Warm the cache before any user traffic arrives. if err := prewarmCache(); err != nil { log.Fatal(err) } // Later, when the user submits a message, the system-prompt prefix is already cached. response, err := respond("How do I implement a binary search tree?") if err != nil { log.Fatal(err) } for _, block := range response.Content { if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok { fmt.Println(textBlock.Text) } } } ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); List systemPrompt = List.of(TextBlockParam.builder() .text("You are an expert software engineer with deep knowledge of distributed systems...") .cacheControl(CacheControlEphemeral.builder().build()) .build()); // Call this at application startup or on a scheduled interval. void prewarmCache() { client.messages().create(MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(0) .systemOfTextBlockParams(systemPrompt) .addUserMessage("warmup") .build()); } // The real user request; benefits from a warm cache. Message respond(String userMessage) { return client.messages().create(MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .systemOfTextBlockParams(systemPrompt) .addUserMessage(userMessage) .build()); } void main() { // Warm the cache before any user traffic arrives. prewarmCache(); // Later, when the user submits a message, the system-prompt prefix is already cached. Message response = respond("How do I implement a binary search tree?"); response.content().stream() .flatMap(block -> block.text().stream()) .forEach(textBlock -> IO.println(textBlock.text())); } ``` ```php PHP $client = new Client(); $systemPrompt = [ [ 'type' => 'text', 'text' => 'You are an expert software engineer with deep knowledge of distributed systems...', 'cache_control' => ['type' => 'ephemeral'], ], ]; // Call this at application startup or on a scheduled interval. $prewarmCache = fn () => $client->messages->create( model: Model::CLAUDE_OPUS_5, maxTokens: 0, system: $systemPrompt, messages: [['role' => 'user', 'content' => 'warmup']], ); // The real user request; benefits from a warm cache. $respond = fn (string $userMessage) => $client->messages->create( model: Model::CLAUDE_OPUS_5, maxTokens: 1024, system: $systemPrompt, messages: [['role' => 'user', 'content' => $userMessage]], ); // Warm the cache before any user traffic arrives. $prewarmCache(); // Later, when the user submits a message, the system-prompt prefix is already cached. $response = $respond('How do I implement a binary search tree?'); foreach ($response->content as $block) { if ($block->type === 'text') { echo $block->text, PHP_EOL; } } ``` ```ruby Ruby client = Anthropic::Client.new SYSTEM_PROMPT = [ { type: "text", text: "You are an expert software engineer with deep knowledge of distributed systems...", cache_control: {type: "ephemeral"} } ] # Call this at application startup or on a scheduled interval. def prewarm_cache(client) client.messages.create( model: Anthropic::Model::CLAUDE_OPUS_5, max_tokens: 0, system_: SYSTEM_PROMPT, messages: [{role: "user", content: "warmup"}] ) end # The real user request; benefits from a warm cache. def respond(client, user_message) client.messages.create( model: Anthropic::Model::CLAUDE_OPUS_5, max_tokens: 1024, system_: SYSTEM_PROMPT, messages: [{role: "user", content: user_message}] ) end # Warm the cache before any user traffic arrives. prewarm_cache(client) # Later, when the user submits a message, the system-prompt prefix is already cached. response = respond(client, "How do I implement a binary search tree?") response.content.each do |block| puts block.text if block.type == :text end ``` Keep in mind that the cache TTL still applies. For the default 5-minute cache, send a new pre-warm request at least every 5 minutes to keep the cache warm. For longer gaps between user requests, use the [1-hour cache duration](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#1-hour-cache-duration) instead. ### Limitations A `max_tokens: 0` request is rejected with an `invalid_request_error` if any of the following are set, since each implies output that a zero-token budget cannot produce: * `stream: true` * [Extended thinking](https://platform.claude.com/docs/en/build-with-claude/extended-thinking) (`thinking.type: "enabled"`) * [Structured outputs](https://platform.claude.com/docs/en/build-with-claude/structured-outputs) (`output_config.format`) * `tool_choice` of `{"type": "tool", ...}` or `{"type": "any"}` `max_tokens: 0` is also rejected inside a [Message Batches](https://platform.claude.com/docs/en/build-with-claude/batch-processing) request. Pre-warming targets time-to-first-token, which does not apply to batch processing, and a cache entry written during batch processing would likely expire before the follow-up request runs. ### Replacing the max\_tokens=1 workaround Before `max_tokens: 0` was available, some applications used `max_tokens: 1` warm-up calls to achieve the same effect. The `max_tokens: 0` approach is preferred: no output is produced, so there is no single-token reply to discard, no output tokens are billed, and the intent of the request is unambiguous. *** ## Prompt caching examples To help you get started with prompt caching, the [prompt caching cookbook](https://platform.claude.com/cookbook/misc-prompt-caching) provides detailed examples and best practices. The following code snippets showcase various prompt caching patterns. These examples demonstrate how to implement caching in different scenarios, helping you understand the practical applications of this feature: ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "system": [ { "type": "text", "text": "You are an AI assistant tasked with analyzing legal documents." }, { "type": "text", "text": "Here is the full text of a complex legal agreement: [Insert full text of a 50-page legal agreement here]", "cache_control": {"type": "ephemeral"} } ], "messages": [ { "role": "user", "content": "What are the key terms and conditions in this agreement?" } ] }' ``` ```bash CLI ant messages create --transform usage <<'YAML' model: claude-opus-5 max_tokens: 1024 system: - type: text text: You are an AI assistant tasked with analyzing legal documents. - type: text text: >- Here is the full text of a complex legal agreement: [Insert full text of a 50-page legal agreement here] cache_control: type: ephemeral messages: - role: user content: What are the key terms and conditions in this agreement? YAML ``` ```python Python client = anthropic.Anthropic() response = client.messages.create( model="claude-opus-5", max_tokens=1024, system=[ { "type": "text", "text": "You are an AI assistant tasked with analyzing legal documents.", }, { "type": "text", "text": "Here is the full text of a complex legal agreement: [Insert full text of a 50-page legal agreement here]", "cache_control": {"type": "ephemeral"}, }, ], messages=[ { "role": "user", "content": "What are the key terms and conditions in this agreement?", } ], ) print(response.usage.model_dump_json()) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, system: [ { type: "text", text: "You are an AI assistant tasked with analyzing legal documents." }, { type: "text", text: "Here is the full text of a complex legal agreement: [Insert full text of a 50-page legal agreement here]", cache_control: { type: "ephemeral" } } ], messages: [ { role: "user", content: "What are the key terms and conditions in this agreement?" } ] }); console.log(response.usage); ``` ```csharp C# AnthropicClient client = new() { ApiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY") }; var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, System = new MessageCreateParamsSystem(new List { new TextBlockParam() { Text = "You are an AI assistant tasked with analyzing legal documents.", }, new TextBlockParam() { Text = "Here is the full text of a complex legal agreement: [Insert full text of a 50-page legal agreement here]", CacheControl = new CacheControlEphemeral(), }, }), Messages = [ new() { Role = Role.User, Content = "What are the key terms and conditions in this agreement?" } ] }; var message = await client.Messages.Create(parameters); Console.WriteLine(message.Usage); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, System: []anthropic.TextBlockParam{ { Text: "You are an AI assistant tasked with analyzing legal documents.", }, { Text: "Here is the full text of a complex legal agreement: [Insert full text of a 50-page legal agreement here]", CacheControl: anthropic.NewCacheControlEphemeralParam(), }, }, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("What are the key terms and conditions in this agreement?")), }, }) if err != nil { log.Fatal(err) } fmt.Println(response.Usage.RawJSON()) ``` ```java Java import com.anthropic.models.messages.CacheControlEphemeral; // ... public class LegalDocumentAnalysisExample { public static void main(String[] args) { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .systemOfTextBlockParams( List.of( TextBlockParam.builder() .text("You are an AI assistant tasked with analyzing legal documents.") .build(), TextBlockParam.builder() .text( "Here is the full text of a complex legal agreement: [Insert full text of a 50-page legal agreement here]" ) .cacheControl(CacheControlEphemeral.builder().build()) .build() ) ) .addUserMessage("What are the key terms and conditions in this agreement?") .build(); Message message = client.messages().create(params); System.out.println(message.usage()); } } ``` ```php PHP $client = new Client(); $message = $client->messages->create( maxTokens: 1024, messages: [ [ 'role' => 'user', 'content' => 'What are the key terms and conditions in this agreement?' ] ], model: 'claude-opus-5', system: [ [ 'type' => 'text', 'text' => 'You are an AI assistant tasked with analyzing legal documents.' ], [ 'type' => 'text', 'text' => 'Here is the full text of a complex legal agreement: [Insert full text of a 50-page legal agreement here]', 'cache_control' => ['type' => 'ephemeral'] ] ], ); echo json_encode($message->usage), PHP_EOL; ``` ```ruby Ruby client = Anthropic::Client.new message = client.messages.create( model: "claude-opus-5", max_tokens: 1024, system: [ { type: "text", text: "You are an AI assistant tasked with analyzing legal documents." }, { type: "text", text: "Here is the full text of a complex legal agreement: [Insert full text of a 50-page legal agreement here]", cache_control: { type: "ephemeral" } } ], messages: [ { role: "user", content: "What are the key terms and conditions in this agreement?" } ] ) puts message.usage ``` This example demonstrates basic prompt caching usage, caching the full text of the legal agreement as a prefix while keeping the user instruction uncached. For the first request: * `input_tokens`: Number of tokens in the user message only * `cache_creation_input_tokens`: Number of tokens in the entire system message, including the legal document * `cache_read_input_tokens`: 0 (no cache hit on first request) For subsequent requests within the cache lifetime: * `input_tokens`: Number of tokens in the user message only * `cache_creation_input_tokens`: 0 (no new cache creation) * `cache_read_input_tokens`: Number of tokens in the entire cached system message Tool definitions can be cached by placing `cache_control` on the last tool in your `tools` array. All tools defined before and including that tool are cached as a single prefix. ```json { "model": "claude-opus-5", "max_tokens": 1024, "tools": [ { "name": "get_weather", "description": "Get the current weather in a given location", "input_schema": { "type": "object", "properties": { "location": { "type": "string" } }, "required": ["location"] } }, { "name": "get_time", "description": "Get the current time in a given time zone", "input_schema": { "type": "object", "properties": { "timezone": { "type": "string" } }, "required": ["timezone"] }, "cache_control": { "type": "ephemeral" } } ], "messages": [{ "role": "user", "content": "What is the weather and time in New York?" }] } ``` On the first request, `cache_creation_input_tokens` reflects the token count of all tool definitions. On subsequent requests within the cache lifetime, those tokens appear under `cache_read_input_tokens` instead. For detailed interaction between tool definitions, `defer_loading`, and cache invalidation, see [Tool use with prompt caching](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-use-with-prompt-caching). ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "system": [ { "type": "text", "text": "...long system prompt", "cache_control": {"type": "ephemeral"} } ], "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Hello, can you tell me more about the solar system?" } ] }, { "role": "assistant", "content": "Certainly! The solar system is the collection of celestial bodies that orbit our Sun. It consists of eight planets, numerous moons, asteroids, comets, and other objects. The planets, in order from closest to farthest from the Sun, are: Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, and Neptune. Each planet has its own unique characteristics and features. Is there a specific aspect of the solar system you would like to know more about?" }, { "role": "user", "content": [ { "type": "text", "text": "Good to know." }, { "type": "text", "text": "Tell me more about Mars.", "cache_control": {"type": "ephemeral"} } ] } ] }' ``` ```bash CLI ant messages create --transform usage <<'YAML' model: claude-opus-5 max_tokens: 1024 system: - type: text text: "...long system prompt" cache_control: type: ephemeral messages: - role: user content: - type: text text: Hello, can you tell me more about the solar system? - role: assistant content: >- Certainly! The solar system is the collection of celestial bodies that orbit our Sun. It consists of eight planets, numerous moons, asteroids, comets, and other objects. The planets, in order from closest to farthest from the Sun, are: Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, and Neptune. Each planet has its own unique characteristics and features. Is there a specific aspect of the solar system you would like to know more about? - role: user content: - type: text text: Good to know. - type: text text: Tell me more about Mars. cache_control: type: ephemeral YAML ``` ```python Python client = anthropic.Anthropic() response = client.messages.create( model="claude-opus-5", max_tokens=1024, system=[ { "type": "text", "text": "...long system prompt", "cache_control": {"type": "ephemeral"}, } ], messages=[ # ...long conversation so far { "role": "user", "content": [ { "type": "text", "text": "Hello, can you tell me more about the solar system?", } ], }, { "role": "assistant", "content": "Certainly! The solar system is the collection of celestial bodies that orbit our Sun. It consists of eight planets, numerous moons, asteroids, comets, and other objects. The planets, in order from closest to farthest from the Sun, are: Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, and Neptune. Each planet has its own unique characteristics and features. Is there a specific aspect of the solar system you'd like to know more about?", }, { "role": "user", "content": [ {"type": "text", "text": "Good to know."}, { "type": "text", "text": "Tell me more about Mars.", "cache_control": {"type": "ephemeral"}, }, ], }, ], ) print(response.usage.model_dump_json()) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, system: [ { type: "text", text: "...long system prompt", cache_control: { type: "ephemeral" } } ], messages: [ // ...long conversation so far { role: "user", content: [ { type: "text", text: "Hello, can you tell me more about the solar system?" } ] }, { role: "assistant", content: "Certainly! The solar system is the collection of celestial bodies that orbit our Sun. It consists of eight planets, numerous moons, asteroids, comets, and other objects. The planets, in order from closest to farthest from the Sun, are: Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, and Neptune. Each planet has its own unique characteristics and features. Is there a specific aspect of the solar system you'd like to know more about?" }, { role: "user", content: [ { type: "text", text: "Good to know." }, { type: "text", text: "Tell me more about Mars.", cache_control: { type: "ephemeral" } } ] } ] }); console.log(response.usage); ``` ```csharp C# AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, System = new MessageCreateParamsSystem(new List { new TextBlockParam() { Text = "...long system prompt", CacheControl = new CacheControlEphemeral(), }, }), Messages = [ new() { Role = Role.User, Content = new MessageParamContent(new List { new ContentBlockParam(new TextBlockParam("Hello, can you tell me more about the solar system?")), }), }, new() { Role = Role.Assistant, Content = "Certainly! The solar system is the collection of celestial bodies that orbit our Sun. It consists of eight planets, numerous moons, asteroids, comets, and other objects. The planets, in order from closest to farthest from the Sun, are: Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, and Neptune. Each planet has its own unique characteristics and features. Is there a specific aspect of the solar system you would like to know more about?" }, new() { Role = Role.User, Content = new MessageParamContent(new List { new ContentBlockParam(new TextBlockParam("Good to know.")), new ContentBlockParam(new TextBlockParam() { Text = "Tell me more about Mars.", CacheControl = new CacheControlEphemeral(), }), }) } ] }; var message = await client.Messages.Create(parameters); Console.WriteLine(message.Usage); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, System: []anthropic.TextBlockParam{ { Text: "...long system prompt", CacheControl: anthropic.NewCacheControlEphemeralParam(), }, }, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Hello, can you tell me more about the solar system?")), anthropic.NewAssistantMessage(anthropic.NewTextBlock("Certainly! The solar system is the collection of celestial bodies that orbit our Sun. It consists of eight planets, numerous moons, asteroids, comets, and other objects. The planets, in order from closest to farthest from the Sun, are: Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, and Neptune. Each planet has its own unique characteristics and features. Is there a specific aspect of the solar system you would like to know more about?")), { Role: anthropic.MessageParamRoleUser, Content: []anthropic.ContentBlockParamUnion{ anthropic.NewTextBlock("Good to know."), {OfText: &anthropic.TextBlockParam{ Text: "Tell me more about Mars.", CacheControl: anthropic.NewCacheControlEphemeralParam(), }}, }, }, }, }) if err != nil { log.Fatal(err) } fmt.Println(response.Usage.RawJSON()) ``` ```java Java import com.anthropic.models.messages.CacheControlEphemeral; // ... public class ConversationWithCacheControlExample { public static void main(String[] args) { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); // Create ephemeral system prompt TextBlockParam systemPrompt = TextBlockParam.builder() .text("...long system prompt") .cacheControl(CacheControlEphemeral.builder().build()) .build(); // Create message params MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .systemOfTextBlockParams(List.of(systemPrompt)) // First user message (without cache control) .addUserMessage("Hello, can you tell me more about the solar system?") // Assistant response .addAssistantMessage( "Certainly! The solar system is the collection of celestial bodies that orbit our Sun. It consists of eight planets, numerous moons, asteroids, comets, and other objects. The planets, in order from closest to farthest from the Sun, are: Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, and Neptune. Each planet has its own unique characteristics and features. Is there a specific aspect of the solar system you would like to know more about?" ) // Second user message (with cache control) .addUserMessageOfBlockParams( List.of( ContentBlockParam.ofText(TextBlockParam.builder().text("Good to know.").build()), ContentBlockParam.ofText( TextBlockParam.builder() .text("Tell me more about Mars.") .cacheControl(CacheControlEphemeral.builder().build()) .build() ) ) ) .build(); Message message = client.messages().create(params); System.out.println(message.usage()); } } ``` ```php PHP $client = new Client(); $message = $client->messages->create( maxTokens: 1024, messages: [ [ 'role' => 'user', 'content' => [ [ 'type' => 'text', 'text' => 'Hello, can you tell me more about the solar system?' ] ] ], [ 'role' => 'assistant', 'content' => "Certainly! The solar system is the collection of celestial bodies that orbit our Sun. It consists of eight planets, numerous moons, asteroids, comets, and other objects. The planets, in order from closest to farthest from the Sun, are: Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, and Neptune. Each planet has its own unique characteristics and features. Is there a specific aspect of the solar system you would like to know more about?" ], [ 'role' => 'user', 'content' => [ ['type' => 'text', 'text' => 'Good to know.'], [ 'type' => 'text', 'text' => 'Tell me more about Mars.', 'cache_control' => ['type' => 'ephemeral'] ] ] ] ], model: 'claude-opus-5', system: [ [ 'type' => 'text', 'text' => '...long system prompt', 'cache_control' => ['type' => 'ephemeral'] ] ], ); echo json_encode($message->usage), PHP_EOL; ``` ```ruby Ruby client = Anthropic::Client.new message = client.messages.create( model: "claude-opus-5", max_tokens: 1024, system: [ { type: "text", text: "...long system prompt", cache_control: { type: "ephemeral" } } ], messages: [ { role: "user", content: [ { type: "text", text: "Hello, can you tell me more about the solar system?" } ] }, { role: "assistant", content: "Certainly! The solar system is the collection of celestial bodies that orbit our Sun. It consists of eight planets, numerous moons, asteroids, comets, and other objects. The planets, in order from closest to farthest from the Sun, are: Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, and Neptune. Each planet has its own unique characteristics and features. Is there a specific aspect of the solar system you would like to know more about?" }, { role: "user", content: [ { type: "text", text: "Good to know." }, { type: "text", text: "Tell me more about Mars.", cache_control: { type: "ephemeral" } } ] } ] ) puts message.usage ``` This example demonstrates how to use prompt caching in a multi-turn conversation. During each turn, the final block of the final message is marked with `cache_control` so the conversation can be incrementally cached. The system automatically looks up and uses the longest previously cached sequence of blocks for follow-up messages. That is, blocks that were previously marked with a `cache_control` block are later not marked with this, but they will still be considered a cache hit (and also a cache refresh!) if they are hit within 5 minutes. In addition, note that the `cache_control` parameter is placed on the system message. This is to ensure that if this gets evicted from the cache (after not being used for more than 5 minutes), it will get added back to the cache on the next request. This approach is useful for maintaining context in ongoing conversations without repeatedly processing the same information. When this is set up properly, you should see the following in the usage response of each request: * `input_tokens`: Number of tokens in the new user message (will be minimal) * `cache_creation_input_tokens`: Number of tokens in the new assistant and user turns * `cache_read_input_tokens`: Number of tokens in the conversation up to the previous turn ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "tools": [ { "name": "search_documents", "description": "Search through the knowledge base", "input_schema": { "type": "object", "properties": { "query": { "type": "string", "description": "Search query" } }, "required": ["query"] } }, { "name": "get_document", "description": "Retrieve a specific document by ID", "input_schema": { "type": "object", "properties": { "doc_id": { "type": "string", "description": "Document ID" } }, "required": ["doc_id"] }, "cache_control": {"type": "ephemeral"} } ], "system": [ { "type": "text", "text": "You are a helpful research assistant with access to a document knowledge base.\n\n# Instructions\n- Always search for relevant documents before answering\n- Provide citations for your sources\n- Be objective and accurate in your responses\n- If multiple documents contain relevant information, synthesize them\n- Acknowledge when information is not available in the knowledge base", "cache_control": {"type": "ephemeral"} }, { "type": "text", "text": "# Knowledge Base Context\n\nHere are the relevant documents for this conversation:\n\n## Document 1: Solar System Overview\nThe solar system consists of the Sun and all objects that orbit it...\n\n## Document 2: Planetary Characteristics\nEach planet has unique features. Mercury is the smallest planet...\n\n## Document 3: Mars Exploration\nMars has been a target of exploration for decades...\n\n[Additional documents...]", "cache_control": {"type": "ephemeral"} } ], "messages": [ { "role": "user", "content": "Can you search for information about Mars rovers?" }, { "role": "assistant", "content": [ { "type": "tool_use", "id": "tool_1", "name": "search_documents", "input": {"query": "Mars rovers"} } ] }, { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "tool_1", "content": "Found 3 relevant documents: Document 3 (Mars Exploration), Document 7 (Rover Technology), Document 9 (Mission History)" } ] }, { "role": "assistant", "content": [ { "type": "text", "text": "I found 3 relevant documents about Mars rovers. Let me get more details from the Mars Exploration document." } ] }, { "role": "user", "content": [ { "type": "text", "text": "Yes, please tell me about the Perseverance rover specifically.", "cache_control": {"type": "ephemeral"} } ] } ] }' ``` ```bash CLI ant messages create --transform usage <<'YAML' model: claude-opus-5 max_tokens: 1024 tools: - name: search_documents description: Search through the knowledge base input_schema: type: object properties: query: type: string description: Search query required: [query] - name: get_document description: Retrieve a specific document by ID input_schema: type: object properties: doc_id: type: string description: Document ID required: [doc_id] cache_control: type: ephemeral system: - type: text text: |- You are a helpful research assistant with access to a document knowledge base. # Instructions - Always search for relevant documents before answering - Provide citations for your sources - Be objective and accurate in your responses - If multiple documents contain relevant information, synthesize them - Acknowledge when information is not available in the knowledge base cache_control: type: ephemeral - type: text text: |- # Knowledge Base Context Here are the relevant documents for this conversation: ## Document 1: Solar System Overview The solar system consists of the Sun and all objects that orbit it... ## Document 2: Planetary Characteristics Each planet has unique features. Mercury is the smallest planet... ## Document 3: Mars Exploration Mars has been a target of exploration for decades... [Additional documents...] cache_control: type: ephemeral messages: - role: user content: Can you search for information about Mars rovers? - role: assistant content: - type: tool_use id: tool_1 name: search_documents input: query: Mars rovers - role: user content: - type: tool_result tool_use_id: tool_1 content: >- Found 3 relevant documents: Document 3 (Mars Exploration), Document 7 (Rover Technology), Document 9 (Mission History) - role: assistant content: - type: text text: >- I found 3 relevant documents about Mars rovers. Let me get more details from the Mars Exploration document. - role: user content: - type: text text: Yes, please tell me about the Perseverance rover specifically. cache_control: type: ephemeral YAML ``` ```python Python client = anthropic.Anthropic() response = client.messages.create( model="claude-opus-5", max_tokens=1024, tools=[ { "name": "search_documents", "description": "Search through the knowledge base", "input_schema": { "type": "object", "properties": { "query": {"type": "string", "description": "Search query"} }, "required": ["query"], }, }, { "name": "get_document", "description": "Retrieve a specific document by ID", "input_schema": { "type": "object", "properties": { "doc_id": {"type": "string", "description": "Document ID"} }, "required": ["doc_id"], }, "cache_control": {"type": "ephemeral"}, }, ], system=[ { "type": "text", "text": "You are a helpful research assistant with access to a document knowledge base.\n\n# Instructions\n- Always search for relevant documents before answering\n- Provide citations for your sources\n- Be objective and accurate in your responses\n- If multiple documents contain relevant information, synthesize them\n- Acknowledge when information is not available in the knowledge base", "cache_control": {"type": "ephemeral"}, }, { "type": "text", "text": "# Knowledge Base Context\n\nHere are the relevant documents for this conversation:\n\n## Document 1: Solar System Overview\nThe solar system consists of the Sun and all objects that orbit it...\n\n## Document 2: Planetary Characteristics\nEach planet has unique features. Mercury is the smallest planet...\n\n## Document 3: Mars Exploration\nMars has been a target of exploration for decades...\n\n[Additional documents...]", "cache_control": {"type": "ephemeral"}, }, ], messages=[ { "role": "user", "content": "Can you search for information about Mars rovers?", }, { "role": "assistant", "content": [ { "type": "tool_use", "id": "tool_1", "name": "search_documents", "input": {"query": "Mars rovers"}, } ], }, { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "tool_1", "content": "Found 3 relevant documents: Document 3 (Mars Exploration), Document 7 (Rover Technology), Document 9 (Mission History)", } ], }, { "role": "assistant", "content": [ { "type": "text", "text": "I found 3 relevant documents about Mars rovers. Let me get more details from the Mars Exploration document.", } ], }, { "role": "user", "content": [ { "type": "text", "text": "Yes, please tell me about the Perseverance rover specifically.", "cache_control": {"type": "ephemeral"}, } ], }, ], ) print(response.usage.model_dump_json()) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, tools: [ { name: "search_documents", description: "Search through the knowledge base", input_schema: { type: "object", properties: { query: { type: "string", description: "Search query" } }, required: ["query"] } }, { name: "get_document", description: "Retrieve a specific document by ID", input_schema: { type: "object", properties: { doc_id: { type: "string", description: "Document ID" } }, required: ["doc_id"] }, cache_control: { type: "ephemeral" } } ], system: [ { type: "text", text: "You are a helpful research assistant with access to a document knowledge base.\n\n# Instructions\n- Always search for relevant documents before answering\n- Provide citations for your sources\n- Be objective and accurate in your responses\n- If multiple documents contain relevant information, synthesize them\n- Acknowledge when information is not available in the knowledge base", cache_control: { type: "ephemeral" } }, { type: "text", text: "# Knowledge Base Context\n\nHere are the relevant documents for this conversation:\n\n## Document 1: Solar System Overview\nThe solar system consists of the Sun and all objects that orbit it...\n\n## Document 2: Planetary Characteristics\nEach planet has unique features. Mercury is the smallest planet...\n\n## Document 3: Mars Exploration\nMars has been a target of exploration for decades...\n\n[Additional documents...]", cache_control: { type: "ephemeral" } } ], messages: [ { role: "user", content: "Can you search for information about Mars rovers?" }, { role: "assistant", content: [ { type: "tool_use", id: "tool_1", name: "search_documents", input: { query: "Mars rovers" } } ] }, { role: "user", content: [ { type: "tool_result", tool_use_id: "tool_1", content: "Found 3 relevant documents: Document 3 (Mars Exploration), Document 7 (Rover Technology), Document 9 (Mission History)" } ] }, { role: "assistant", content: [ { type: "text", text: "I found 3 relevant documents about Mars rovers. Let me get more details from the Mars Exploration document." } ] }, { role: "user", content: [ { type: "text", text: "Yes, please tell me about the Perseverance rover specifically.", cache_control: { type: "ephemeral" } } ] } ] }); console.log(response.usage); ``` ```csharp C# AnthropicClient client = new() { ApiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY") }; var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Tools = [ new ToolUnion(new Tool() { Name = "search_documents", Description = "Search through the knowledge base", InputSchema = new InputSchema() { Properties = new Dictionary { ["query"] = JsonSerializer.SerializeToElement(new { type = "string", description = "Search query" }), }, Required = ["query"], }, }), new ToolUnion(new Tool() { Name = "get_document", Description = "Retrieve a specific document by ID", InputSchema = new InputSchema() { Properties = new Dictionary { ["doc_id"] = JsonSerializer.SerializeToElement(new { type = "string", description = "Document ID" }), }, Required = ["doc_id"], }, CacheControl = new CacheControlEphemeral(), }), ], System = new MessageCreateParamsSystem(new List { new TextBlockParam() { Text = "You are a helpful research assistant with access to a document knowledge base.\n\n# Instructions\n- Always search for relevant documents before answering\n- Provide citations for your sources\n- Be objective and accurate in your responses\n- If multiple documents contain relevant information, synthesize them\n- Acknowledge when information is not available in the knowledge base", CacheControl = new CacheControlEphemeral(), }, new TextBlockParam() { Text = "# Knowledge Base Context\n\nHere are the relevant documents for this conversation:\n\n## Document 1: Solar System Overview\nThe solar system consists of the Sun and all objects that orbit it...\n\n## Document 2: Planetary Characteristics\nEach planet has unique features. Mercury is the smallest planet...\n\n## Document 3: Mars Exploration\nMars has been a target of exploration for decades...\n\n[Additional documents...]", CacheControl = new CacheControlEphemeral(), }, }), Messages = [ new() { Role = Role.User, Content = "Can you search for information about Mars rovers?" }, new() { Role = Role.Assistant, Content = new MessageParamContent(new List { new ContentBlockParam(new ToolUseBlockParam() { ID = "tool_1", Name = "search_documents", Input = new Dictionary { ["query"] = JsonSerializer.SerializeToElement("Mars rovers"), }, }), }), }, new() { Role = Role.User, Content = new MessageParamContent(new List { new ContentBlockParam(new ToolResultBlockParam() { ToolUseID = "tool_1", Content = "Found 3 relevant documents: Document 3 (Mars Exploration), Document 7 (Rover Technology), Document 9 (Mission History)", }), }), }, new() { Role = Role.Assistant, Content = "I found 3 relevant documents about Mars rovers. Let me get more details from the Mars Exploration document.", }, new() { Role = Role.User, Content = new MessageParamContent(new List { new ContentBlockParam(new TextBlockParam() { Text = "Yes, please tell me about the Perseverance rover specifically.", CacheControl = new CacheControlEphemeral(), }), }), }, ] }; var message = await client.Messages.Create(parameters); Console.WriteLine(message.Usage); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Tools: []anthropic.ToolUnionParam{ {OfTool: &anthropic.ToolParam{ Name: "search_documents", Description: anthropic.String("Search through the knowledge base"), InputSchema: anthropic.ToolInputSchemaParam{ Properties: map[string]any{ "query": map[string]any{ "type": "string", "description": "Search query", }, }, Required: []string{"query"}, }, }}, {OfTool: &anthropic.ToolParam{ Name: "get_document", Description: anthropic.String("Retrieve a specific document by ID"), InputSchema: anthropic.ToolInputSchemaParam{ Properties: map[string]any{ "doc_id": map[string]any{ "type": "string", "description": "Document ID", }, }, Required: []string{"doc_id"}, }, CacheControl: anthropic.NewCacheControlEphemeralParam(), }}, }, System: []anthropic.TextBlockParam{ { Text: "You are a helpful research assistant with access to a document knowledge base.\n\n# Instructions\n- Always search for relevant documents before answering\n- Provide citations for your sources\n- Be objective and accurate in your responses\n- If multiple documents contain relevant information, synthesize them\n- Acknowledge when information is not available in the knowledge base", CacheControl: anthropic.NewCacheControlEphemeralParam(), }, { Text: "# Knowledge Base Context\n\nHere are the relevant documents for this conversation:\n\n## Document 1: Solar System Overview\nThe solar system consists of the Sun and all objects that orbit it...\n\n## Document 2: Planetary Characteristics\nEach planet has unique features. Mercury is the smallest planet...\n\n## Document 3: Mars Exploration\nMars has been a target of exploration for decades...\n\n[Additional documents...]", CacheControl: anthropic.NewCacheControlEphemeralParam(), }, }, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Can you search for information about Mars rovers?")), anthropic.NewAssistantMessage(anthropic.NewToolUseBlock( "tool_1", map[string]any{"query": "Mars rovers"}, "search_documents", )), anthropic.NewUserMessage(anthropic.NewToolResultBlock( "tool_1", "Found 3 relevant documents: Document 3 (Mars Exploration), Document 7 (Rover Technology), Document 9 (Mission History)", false, )), anthropic.NewAssistantMessage(anthropic.NewTextBlock("I found 3 relevant documents about Mars rovers. Let me get more details from the Mars Exploration document.")), { Role: anthropic.MessageParamRoleUser, Content: []anthropic.ContentBlockParamUnion{ {OfText: &anthropic.TextBlockParam{ Text: "Yes, please tell me about the Perseverance rover specifically.", CacheControl: anthropic.NewCacheControlEphemeralParam(), }}, }, }, }, }) if err != nil { log.Fatal(err) } fmt.Println(response.Usage.RawJSON()) ``` ```java Java import com.anthropic.models.messages.CacheControlEphemeral; // ... public class MultipleCacheBreakpointsExample { public static void main(String[] args) { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); // Search tool schema InputSchema searchSchema = InputSchema.builder() .properties( JsonValue.from( Map.of("query", Map.of("type", "string", "description", "Search query")) ) ) .putAdditionalProperty("required", JsonValue.from(List.of("query"))) .build(); // Get document tool schema InputSchema getDocSchema = InputSchema.builder() .properties( JsonValue.from( Map.of("doc_id", Map.of("type", "string", "description", "Document ID")) ) ) .putAdditionalProperty("required", JsonValue.from(List.of("doc_id"))) .build(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) // Tools with cache control on the last one .addTool( Tool.builder() .name("search_documents") .description("Search through the knowledge base") .inputSchema(searchSchema) .build() ) .addTool( Tool.builder() .name("get_document") .description("Retrieve a specific document by ID") .inputSchema(getDocSchema) .cacheControl(CacheControlEphemeral.builder().build()) .build() ) // System prompts with cache control on instructions and context separately .systemOfTextBlockParams( List.of( TextBlockParam.builder() .text( "You are a helpful research assistant with access to a document knowledge base.\n\n# Instructions\n- Always search for relevant documents before answering\n- Provide citations for your sources\n- Be objective and accurate in your responses\n- If multiple documents contain relevant information, synthesize them\n- Acknowledge when information is not available in the knowledge base" ) .cacheControl(CacheControlEphemeral.builder().build()) .build(), TextBlockParam.builder() .text( "# Knowledge Base Context\n\nHere are the relevant documents for this conversation:\n\n## Document 1: Solar System Overview\nThe solar system consists of the Sun and all objects that orbit it...\n\n## Document 2: Planetary Characteristics\nEach planet has unique features. Mercury is the smallest planet...\n\n## Document 3: Mars Exploration\nMars has been a target of exploration for decades...\n\n[Additional documents...]" ) .cacheControl(CacheControlEphemeral.builder().build()) .build() ) ) // Conversation history .addUserMessage("Can you search for information about Mars rovers?") .addAssistantMessageOfBlockParams( List.of( ContentBlockParam.ofToolUse( ToolUseBlockParam.builder() .id("tool_1") .name("search_documents") .input(JsonValue.from(Map.of("query", "Mars rovers"))) .build() ) ) ) .addUserMessageOfBlockParams( List.of( ContentBlockParam.ofToolResult( ToolResultBlockParam.builder() .toolUseId("tool_1") .content( "Found 3 relevant documents: Document 3 (Mars Exploration), Document 7 (Rover Technology), Document 9 (Mission History)" ) .build() ) ) ) .addAssistantMessageOfBlockParams( List.of( ContentBlockParam.ofText( TextBlockParam.builder() .text( "I found 3 relevant documents about Mars rovers. Let me get more details from the Mars Exploration document." ) .build() ) ) ) .addUserMessageOfBlockParams( List.of( ContentBlockParam.ofText( TextBlockParam.builder() .text("Yes, please tell me about the Perseverance rover specifically.") .cacheControl(CacheControlEphemeral.builder().build()) .build() ) ) ) .build(); Message message = client.messages().create(params); System.out.println(message.usage()); } } ``` ```php PHP $client = new Client(); $message = $client->messages->create( maxTokens: 1024, messages: [ [ 'role' => 'user', 'content' => 'Can you search for information about Mars rovers?' ], [ 'role' => 'assistant', 'content' => [ [ 'type' => 'tool_use', 'id' => 'tool_1', 'name' => 'search_documents', 'input' => ['query' => 'Mars rovers'] ] ] ], [ 'role' => 'user', 'content' => [ [ 'type' => 'tool_result', 'tool_use_id' => 'tool_1', 'content' => 'Found 3 relevant documents: Document 3 (Mars Exploration), Document 7 (Rover Technology), Document 9 (Mission History)' ] ] ], [ 'role' => 'assistant', 'content' => [ [ 'type' => 'text', 'text' => 'I found 3 relevant documents about Mars rovers. Let me get more details from the Mars Exploration document.' ] ] ], [ 'role' => 'user', 'content' => [ [ 'type' => 'text', 'text' => 'Yes, please tell me about the Perseverance rover specifically.', 'cache_control' => ['type' => 'ephemeral'] ] ] ] ], model: 'claude-opus-5', system: [ [ 'type' => 'text', 'text' => "You are a helpful research assistant with access to a document knowledge base.\n\n# Instructions\n- Always search for relevant documents before answering\n- Provide citations for your sources\n- Be objective and accurate in your responses\n- If multiple documents contain relevant information, synthesize them\n- Acknowledge when information is not available in the knowledge base", 'cache_control' => ['type' => 'ephemeral'] ], [ 'type' => 'text', 'text' => "# Knowledge Base Context\n\nHere are the relevant documents for this conversation:\n\n## Document 1: Solar System Overview\nThe solar system consists of the Sun and all objects that orbit it...\n\n## Document 2: Planetary Characteristics\nEach planet has unique features. Mercury is the smallest planet...\n\n## Document 3: Mars Exploration\nMars has been a target of exploration for decades...\n\n[Additional documents...]", 'cache_control' => ['type' => 'ephemeral'] ] ], tools: [ [ 'name' => 'search_documents', 'description' => 'Search through the knowledge base', 'input_schema' => [ 'type' => 'object', 'properties' => [ 'query' => [ 'type' => 'string', 'description' => 'Search query' ] ], 'required' => ['query'] ] ], [ 'name' => 'get_document', 'description' => 'Retrieve a specific document by ID', 'input_schema' => [ 'type' => 'object', 'properties' => [ 'doc_id' => [ 'type' => 'string', 'description' => 'Document ID' ] ], 'required' => ['doc_id'] ], 'cache_control' => ['type' => 'ephemeral'] ] ], ); echo json_encode($message->usage), PHP_EOL; ``` ```ruby Ruby client = Anthropic::Client.new message = client.messages.create( model: "claude-opus-5", max_tokens: 1024, tools: [ { name: "search_documents", description: "Search through the knowledge base", input_schema: { type: "object", properties: { query: { type: "string", description: "Search query" } }, required: ["query"] } }, { name: "get_document", description: "Retrieve a specific document by ID", input_schema: { type: "object", properties: { doc_id: { type: "string", description: "Document ID" } }, required: ["doc_id"] }, cache_control: { type: "ephemeral" } } ], system: [ { type: "text", text: "You are a helpful research assistant with access to a document knowledge base.\n\n# Instructions\n- Always search for relevant documents before answering\n- Provide citations for your sources\n- Be objective and accurate in your responses\n- If multiple documents contain relevant information, synthesize them\n- Acknowledge when information is not available in the knowledge base", cache_control: { type: "ephemeral" } }, { type: "text", text: "# Knowledge Base Context\n\nHere are the relevant documents for this conversation:\n\n## Document 1: Solar System Overview\nThe solar system consists of the Sun and all objects that orbit it...\n\n## Document 2: Planetary Characteristics\nEach planet has unique features. Mercury is the smallest planet...\n\n## Document 3: Mars Exploration\nMars has been a target of exploration for decades...\n\n[Additional documents...]", cache_control: { type: "ephemeral" } } ], messages: [ { role: "user", content: "Can you search for information about Mars rovers?" }, { role: "assistant", content: [ { type: "tool_use", id: "tool_1", name: "search_documents", input: { query: "Mars rovers" } } ] }, { role: "user", content: [ { type: "tool_result", tool_use_id: "tool_1", content: "Found 3 relevant documents: Document 3 (Mars Exploration), Document 7 (Rover Technology), Document 9 (Mission History)" } ] }, { role: "assistant", content: [ { type: "text", text: "I found 3 relevant documents about Mars rovers. Let me get more details from the Mars Exploration document." } ] }, { role: "user", content: [ { type: "text", text: "Yes, please tell me about the Perseverance rover specifically.", cache_control: { type: "ephemeral" } } ] } ] ) puts message.usage ``` This comprehensive example demonstrates how to use all 4 available cache breakpoints to optimize different parts of your prompt: 1. **Tools cache** (cache breakpoint 1): The `cache_control` parameter on the last tool definition caches all tool definitions. 2. **Reusable instructions cache** (cache breakpoint 2): The static instructions in the system prompt are cached separately. These instructions rarely change between requests. 3. **RAG context cache** (cache breakpoint 3): The knowledge base documents are cached independently, allowing you to update the RAG documents without invalidating the tools or instructions cache. 4. **Conversation history cache** (cache breakpoint 4): The final user message is marked with `cache_control` to enable incremental caching of the conversation as it progresses. This approach provides maximum flexibility: * If you append a new turn to the conversation without changing earlier content, all four cache segments are reused * If you update the RAG documents but keep the same tools and instructions, the first two cache segments are reused * If you change the conversation but keep the same tools, instructions, and documents, the first three segments are reused * Changes at any breakpoint invalidate that segment and everything after it, while earlier cached segments remain valid For the first request: * `input_tokens`: Minimal (tokens after the final cache breakpoint, near 0 in this example) * `cache_creation_input_tokens`: Tokens in all cached segments (tools + instructions + RAG documents + conversation history) * `cache_read_input_tokens`: 0 (no cache hits) For subsequent requests with only a new user message (and the fourth breakpoint moved to that new final message, as in the example): * `input_tokens`: Minimal (tokens after the final cache breakpoint, near 0 in this example) * `cache_creation_input_tokens`: Tokens in the new user message and the previous assistant turn (the new conversation segment being cached) * `cache_read_input_tokens`: All previously cached tokens (tools + instructions + RAG documents + previous conversation) This pattern is especially powerful for: * RAG applications with large document contexts * Agent systems that use multiple tools * Long-running conversations that need to maintain context * Applications that need to optimize different parts of the prompt independently ## Data retention Prompt caching (both automatic and explicit) is ZDR eligible. Anthropic does not store the raw text of your prompts or Claude's responses. KV (key-value) cache representations and cryptographic hashes of cached content are held in memory only and are not stored at rest. Cached entries have a minimum lifetime of 5 minutes (standard) or 1 hour (extended), after which they are promptly, though not immediately, deleted. Cache entries are isolated between organizations and, on the Claude API, Claude Platform on AWS, and Microsoft Foundry, between workspaces within an organization. For ZDR eligibility across all features, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention). *** ## FAQ **In most cases, a single cache breakpoint at the end of your static content is sufficient.** Cache writes happen only at the block you mark. Place it on the last block that stays identical across requests, and every subsequent request reads that same entry. If a later block varies per request (a timestamp, the incoming message), keep the breakpoint before it, on the last stable block. You only need multiple breakpoints if: * A growing conversation pushes your breakpoint 20 or more blocks past the last cache write, putting the prior entry outside the lookback window * You want to cache sections that update at different frequencies independently * You need explicit control over what gets cached for cost optimization Example: If you have system instructions (rarely change) and RAG context (changes daily), you might use two breakpoints to cache them separately. No, cache breakpoints themselves are free. You only pay for: * Writing content to cache (25% more than base input tokens for 5-minute TTL) * Reading from cache (10% of base input token price) * Regular input tokens for uncached content The number of breakpoints doesn't affect pricing - only the amount of content cached and read matters. The usage response includes three separate input token fields that together represent your total input: ```text wrap total_input_tokens = cache_read_input_tokens + cache_creation_input_tokens + input_tokens ``` * `cache_read_input_tokens`: Tokens retrieved from cache (everything before cache breakpoints that was cached) * `cache_creation_input_tokens`: New tokens being written to cache (at cache breakpoints) * `input_tokens`: Tokens **after the last cache breakpoint** that aren't cached **Important:** `input_tokens` does NOT represent all input tokens - only the portion after your last cache breakpoint. If you have cached content, `input_tokens` will typically be much smaller than your total input. **Example:** With a 200k token document cached and a 50 token user question: * `cache_read_input_tokens`: 200,000 * `cache_creation_input_tokens`: 0 * `input_tokens`: 50 * **Total:** 200,050 tokens This breakdown is critical for understanding both your costs and rate limit usage. See [Tracking cache performance](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#tracking-cache-performance) for more details. The cache's default minimum lifetime (TTL) is 5 minutes. This lifetime is refreshed each time the cached content is used. If you find that 5 minutes is too short, Anthropic also offers a [1-hour cache TTL](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#1-hour-cache-duration). The lifetime is measured from the start of the request that writes or reads the cache entry, not from the end of its response. Time spent generating a response counts against the lifetime, so the window for a follow-up request to reuse the cache is the lifetime minus the generation time. If your requests produce long responses and the next request might not start until after the lifetime elapses, use the [1-hour cache TTL](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#1-hour-cache-duration). You can define up to 4 cache breakpoints (using `cache_control` parameters) in your prompt. Prompt caching is supported on all [active Claude models](https://platform.claude.com/docs/en/about-claude/models/overview). Changing thinking parameters (switching modes, or changing the budget in extended mode) invalidates cached message prefixes, and can invalidate cached system prompts and tools as well, because the thinking configuration is rendered into the prompt. The [`output_config.effort`](https://platform.claude.com/docs/en/build-with-claude/effort) value behaves the same way. For more details on cache invalidation, see [What invalidates the cache](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#what-invalidates-the-cache). For more on thinking, including its interaction with tool use and prompt caching, see [Thinking and prompt caching](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-and-prompt-caching). The easiest way is to add `"cache_control": {"type": "ephemeral"}` at the top level of your request body ([automatic caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#automatic-caching)). Alternatively, include at least one `cache_control` breakpoint on individual content blocks ([explicit cache breakpoints](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#explicit-cache-breakpoints)). Yes, prompt caching can be used alongside other API features like tool use and vision capabilities. However, changing whether there are images in a prompt or modifying tool use settings will break the cache. For more details on cache invalidation, see [What invalidates the cache](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#what-invalidates-the-cache). Prompt caching introduces a new pricing structure where 5-minute cache writes cost 25% more than base input tokens, 1-hour cache writes cost 2x base input tokens, and cache hits cost only 10% of the base input token price. Currently, there's no way to manually clear the cache. Cached prefixes automatically expire after a minimum of 5 minutes of inactivity. You can monitor cache performance using the `cache_creation_input_tokens` and `cache_read_input_tokens` fields in the API response. See [What invalidates the cache](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#what-invalidates-the-cache) for more details on cache invalidation, including a list of changes that require creating a new cache entry. Prompt caching is designed with strong privacy and data separation measures: 1. Cache keys are generated using a cryptographic hash of the prompts up to the cache control point. This means only requests with identical prompts can access a specific cache. 2. On the Claude API, Claude Platform on AWS, and Microsoft Foundry, caches are isolated per workspace within an organization. On Bedrock and Google Cloud, caches are isolated per organization. In every case, caches are never shared across organizations, even for identical prompts. See [Cache storage and sharing](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#cache-storage-and-sharing) for details. 3. The caching mechanism is designed to maintain the integrity and privacy of each unique conversation or context. 4. It's safe to use `cache_control` anywhere in your prompts. For caching to produce reads, place the breakpoint at the end of a stable prefix: placing it on a block that changes every request (such as a timestamp or the user's arbitrary input) writes a fresh entry each time and never hits. These measures ensure that prompt caching maintains data privacy and security while offering performance benefits. Yes, it is possible to use prompt caching with your [Batches API](https://platform.claude.com/docs/en/build-with-claude/batch-processing) requests. However, because asynchronous batch requests can be processed concurrently and in any order, cache hits are provided on a best-effort basis. The [1-hour cache](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#1-hour-cache-duration) can help improve your cache hits. The most cost effective way of using it is the following: * Gather a set of message requests that have a shared prefix. * Send a batch request with a single request that has this shared prefix and a 1-hour cache block. This writes the prefix to the 1-hour cache. * As soon as this is complete, submit the rest of the requests. You will have to monitor the job to know when it completes. This is typically better than using the 5-minute cache because it's common for batch requests to take between 5 minutes and 1 hour to complete. This error typically appears when you have upgraded your SDK or you are using outdated code examples. Prompt caching no longer requires the beta prefix. Instead of: ```python Python client.beta.prompt_caching.messages.create(**params) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.beta.promptCaching.messages.create({ model: "claude-opus-5", max_tokens: 1024, system: [ { type: "text", text: "You are an expert on this large document...", cache_control: { type: "ephemeral" } } ], messages: [{ role: "user", content: "Summarize the key points" }] }); console.log(response); ``` ```php PHP $client = new Client(); $message = $client->beta->promptCaching->messages->create( maxTokens: 1024, messages: [ ['role' => 'user', 'content' => 'Summarize the key points'] ], model: 'claude-opus-5', system: [ [ 'type' => 'text', 'text' => 'You are an expert on this large document...', 'cache_control' => ['type' => 'ephemeral'] ] ], ); echo json_encode($message, JSON_PRETTY_PRINT), PHP_EOL; ``` ```ruby Ruby client = Anthropic::Client.new message = client.beta.prompt_caching.messages.create( model: "claude-opus-5", max_tokens: 1024, system: [ { type: "text", text: "You are an expert on this large document...", cache_control: { type: "ephemeral" } } ], messages: [ { role: "user", content: "Summarize the key points" } ] ) puts message.content.find { it.type == :text }.text ``` Use: ```python Python client.messages.create(**params) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, system: [ { type: "text", text: "You are an expert on this large document...", cache_control: { type: "ephemeral" } } ], messages: [{ role: "user", content: "Summarize the key points" }] }); console.log(response); ``` ```php PHP $client = new Client(); $message = $client->messages->create( maxTokens: 1024, messages: [ ['role' => 'user', 'content' => 'Summarize the key points'] ], model: 'claude-opus-5', system: [ [ 'type' => 'text', 'text' => 'You are an expert on this large document...', 'cache_control' => ['type' => 'ephemeral'] ] ], ); echo json_encode($message, JSON_PRETTY_PRINT), PHP_EOL; ``` ```ruby Ruby client = Anthropic::Client.new message = client.messages.create( model: "claude-opus-5", max_tokens: 1024, system: [ { type: "text", text: "You are an expert on this large document...", cache_control: { type: "ephemeral" } } ], messages: [ { role: "user", content: "Summarize the key points" } ] ) puts message ``` This error typically appears when you have upgraded your SDK or you are using outdated code examples. Prompt caching no longer requires the beta prefix. Instead of: ```typescript TypeScript client.beta.promptCaching.messages.create(/* ... */); ``` Simply use: ```typescript client.messages.create(/* ... */); ``` --- title: Token counting url: https://platform.claude.com/docs/en/build-with-claude/token-counting description: Count the tokens in a message before you send it to Claude. Use token counts to manage rate limits and costs, make model routing decisions, and fit prompts to a target length. --- ## Compatibility - [ZDR](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention): eligible (excludes [Covered Models](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#model-specific-data-retention-requirements)) - Platforms: Claude API, Claude Platform on AWS, Amazon Bedrock, Google Cloud, Microsoft Foundry Token counting lets you determine the number of tokens in a message before you send it to Claude. This helps you make informed decisions about your prompts and usage. With token counting, you can: * Proactively manage rate limits and costs * Make smart model routing decisions * Optimize prompts to a specific length *** ## How to count message tokens The [token counting](https://platform.claude.com/docs/en/api/messages-count-tokens) endpoint accepts the same structured list of inputs for creating a message, including support for system prompts, [tools](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview), [images](https://platform.claude.com/docs/en/build-with-claude/vision), and [PDFs](https://platform.claude.com/docs/en/build-with-claude/pdf-support). The response contains the total number of input tokens. The token count is an **estimate**. In some cases, the actual number of input tokens used when creating a message might differ by a small amount. Token counts may include tokens added automatically by Anthropic for system optimizations. **You are not billed for system-added tokens**. Billing reflects only your content. ### Supported models All [active models](https://platform.claude.com/docs/en/about-claude/models/overview) support token counting, including Claude Opus 5 and Claude Sonnet 5. Claude 4.7 and later models and Claude Mythos Preview use a newer tokenizer. The same input text produces approximately 30 percent more tokens than on earlier models. The exact increase depends on the content and workload shape. Recount prompts against the model you plan to use rather than reusing counts measured against earlier models. ### Count tokens in basic messages ```bash cURL curl https://api.anthropic.com/v1/messages/count_tokens \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "content-type: application/json" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-opus-5", "system": "You are a scientist", "messages": [{ "role": "user", "content": "Hello, Claude" }] }' ``` ```bash CLI ant messages count-tokens \ --model claude-opus-5 \ --system "You are a scientist" \ --message '{role: user, content: "Hello, Claude"}' ``` ```python Python client = anthropic.Anthropic() response = client.messages.count_tokens( model="claude-opus-5", system="You are a scientist", messages=[{"role": "user", "content": "Hello, Claude"}], ) print(response.json()) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.messages.countTokens({ model: "claude-opus-5", system: "You are a scientist", messages: [ { role: "user", content: "Hello, Claude" } ] }); console.log(response); ``` ```csharp C# using System; using System.Threading.Tasks; using Anthropic; using Anthropic.Models.Messages; AnthropicClient client = new(); var parameters = new MessageCountTokensParams { Model = Model.ClaudeOpus5, System = "You are a scientist", Messages = [new() { Role = Role.User, Content = "Hello, Claude" }] }; var response = await client.Messages.CountTokens(parameters); Console.WriteLine(response); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.CountTokens(context.TODO(), anthropic.MessageCountTokensParams{ Model: anthropic.ModelClaudeOpus5, System: anthropic.MessageCountTokensParamsSystemUnion{ OfString: anthropic.String("You are a scientist"), }, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Hello, Claude")), }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java import com.anthropic.models.messages.MessageCountTokensParams; import com.anthropic.models.messages.MessageTokensCount; // ... public class CountTokensExample { public static void main(String[] args) { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCountTokensParams params = MessageCountTokensParams.builder() .model(Model.CLAUDE_OPUS_5) .system("You are a scientist") .addUserMessage("Hello, Claude") .build(); MessageTokensCount count = client.messages().countTokens(params); System.out.println(count); } } ``` ```php PHP $client = new Client(); $response = $client->messages->countTokens( messages: [ ['role' => 'user', 'content' => 'Hello, Claude'] ], model: 'claude-opus-5', system: 'You are a scientist', ); echo json_encode($response); ``` ```ruby Ruby client = Anthropic::Client.new response = client.messages.count_tokens( model: "claude-opus-5", system: "You are a scientist", messages: [ { role: "user", content: "Hello, Claude" } ] ) puts response ``` ```json Output { "input_tokens": 14 } ``` ### Count tokens in messages with tools [Server tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/server-tools) token counts only apply to the first sampling call. ```bash cURL curl https://api.anthropic.com/v1/messages/count_tokens \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "content-type: application/json" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-opus-5", "tools": [ { "name": "get_weather", "description": "Get the current weather in a given location", "input_schema": { "type": "object", "properties": { "location": { "type": "string", "description": "The city and state, e.g. San Francisco, CA" } }, "required": ["location"] } } ], "messages": [ { "role": "user", "content": "What'\''s the weather like in San Francisco?" } ] }' ``` ```bash CLI ant messages count-tokens <<'YAML' model: claude-opus-5 tools: - name: get_weather description: Get the current weather in a given location input_schema: type: object properties: location: type: string description: The city and state, e.g. San Francisco, CA required: - location messages: - role: user content: What's the weather like in San Francisco? YAML ``` ```python Python client = anthropic.Anthropic() response = client.messages.count_tokens( model="claude-opus-5", tools=[ { "name": "get_weather", "description": "Get the current weather in a given location", "input_schema": { "type": "object", "properties": { "location": { "type": "string", "description": "The city and state, e.g. San Francisco, CA", } }, "required": ["location"], }, } ], messages=[{"role": "user", "content": "What's the weather like in San Francisco?"}], ) print(response.json()) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.messages.countTokens({ model: "claude-opus-5", tools: [ { name: "get_weather", description: "Get the current weather in a given location", input_schema: { type: "object", properties: { location: { type: "string", description: "The city and state, e.g. San Francisco, CA" } }, required: ["location"] } } ], messages: [{ role: "user", content: "What's the weather like in San Francisco?" }] }); console.log(response); ``` ```csharp C# using System; using System.Collections.Generic; using System.Text.Json; using System.Threading.Tasks; using Anthropic; using Anthropic.Models.Messages; AnthropicClient client = new(); var parameters = new MessageCountTokensParams { Model = Model.ClaudeOpus5, Tools = [ new MessageCountTokensTool(new Tool() { Name = "get_weather", Description = "Get the current weather in a given location", InputSchema = new InputSchema() { Properties = new Dictionary { ["location"] = JsonSerializer.SerializeToElement(new { type = "string", description = "The city and state, e.g. San Francisco, CA" }), }, Required = ["location"], }, }), ], Messages = [new() { Role = Role.User, Content = "What's the weather like in San Francisco?" }] }; var count = await client.Messages.CountTokens(parameters); Console.WriteLine(count); ``` ```go Go client := anthropic.NewClient() response, err := client.Messages.CountTokens(context.TODO(), anthropic.MessageCountTokensParams{ Model: anthropic.ModelClaudeOpus5, Tools: []anthropic.MessageCountTokensToolUnionParam{ {OfTool: &anthropic.ToolParam{ Name: "get_weather", Description: anthropic.String("Get the current weather in a given location"), InputSchema: anthropic.ToolInputSchemaParam{ Properties: map[string]any{ "location": map[string]any{ "type": "string", "description": "The city and state, e.g. San Francisco, CA", }, }, Required: []string{"location"}, }, }}, }, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("What's the weather like in San Francisco?")), }, }) if err != nil { log.Fatal(err) } jsonData, _ := json.MarshalIndent(response, "", " ") fmt.Println(string(jsonData)) ``` ```java Java import com.anthropic.models.messages.MessageCountTokensParams; import com.anthropic.models.messages.MessageTokensCount; // ... AnthropicClient client = AnthropicOkHttpClient.fromEnv(); InputSchema schema = InputSchema.builder() .properties( JsonValue.from( Map.of( "location", Map.of( "type", "string", "description", "The city and state, e.g. San Francisco, CA" ) ) ) ) .putAdditionalProperty("required", JsonValue.from(List.of("location"))) .build(); MessageCountTokensParams params = MessageCountTokensParams.builder() .model(Model.CLAUDE_OPUS_5) .addTool( Tool.builder() .name("get_weather") .description("Get the current weather in a given location") .inputSchema(schema) .build() ) .addUserMessage("What's the weather like in San Francisco?") .build(); MessageTokensCount count = client.messages().countTokens(params); System.out.println(count); ``` ```php PHP $client = new Client(); $response = $client->messages->countTokens( messages: [ ['role' => 'user', 'content' => "What's the weather like in San Francisco?"] ], model: 'claude-opus-5', tools: [ [ 'name' => 'get_weather', 'description' => 'Get the current weather in a given location', 'input_schema' => [ 'type' => 'object', 'properties' => [ 'location' => [ 'type' => 'string', 'description' => 'The city and state, e.g. San Francisco, CA' ] ], 'required' => ['location'] ] ] ], ); echo json_encode($response, JSON_PRETTY_PRINT); ``` ```ruby Ruby client = Anthropic::Client.new response = client.messages.count_tokens( model: "claude-opus-5", tools: [ { name: "get_weather", description: "Get the current weather in a given location", input_schema: { type: "object", properties: { location: { type: "string", description: "The city and state, e.g. San Francisco, CA" } }, required: ["location"] } } ], messages: [ { role: "user", content: "What's the weather like in San Francisco?" } ] ) puts response ``` ```json Output { "input_tokens": 403 } ``` ### Count tokens in messages with images ```bash cURL #!/bin/sh IMAGE_URL="https://platform.claude.com/docs/images/vision-example.jpg" IMAGE_MEDIA_TYPE="image/jpeg" IMAGE_BASE64=$(curl -s "$IMAGE_URL" | base64 | tr -d '\n') curl https://api.anthropic.com/v1/messages/count_tokens \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d @- < { new ContentBlockParam(new ImageBlockParam( new ImageBlockParamSource(new Base64ImageSource() { Data = imageData, MediaType = MediaType.ImageJpeg, }) )), new ContentBlockParam(new TextBlockParam("Describe this image")), }), } ] }; var count = await client.Messages.CountTokens(parameters); Console.WriteLine(count); ``` ```go Go imageURL := "https://platform.claude.com/docs/images/vision-example.jpg" req, err := http.NewRequest("GET", imageURL, nil) if err != nil { log.Fatal(err) } req.Header.Set("User-Agent", "AnthropicDocsBot/1.0") resp, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } defer resp.Body.Close() imageBytes, err := io.ReadAll(resp.Body) if err != nil { log.Fatal(err) } imageData := base64.StdEncoding.EncodeToString(imageBytes) client := anthropic.NewClient() response, err := client.Messages.CountTokens(context.TODO(), anthropic.MessageCountTokensParams{ Model: anthropic.ModelClaudeOpus5, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage( anthropic.NewImageBlockBase64("image/jpeg", imageData), anthropic.NewTextBlock("Describe this image"), ), }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java import com.anthropic.models.messages.Base64ImageSource; // ... import com.anthropic.models.messages.MessageCountTokensParams; import com.anthropic.models.messages.MessageTokensCount; // ... AnthropicClient client = AnthropicOkHttpClient.fromEnv(); String imageUrl = "https://platform.claude.com/docs/images/vision-example.jpg"; String imageMediaType = "image/jpeg"; HttpClient httpClient = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder().uri(URI.create(imageUrl)).build(); byte[] imageBytes = httpClient .send(request, HttpResponse.BodyHandlers.ofByteArray()) .body(); String imageBase64 = Base64.getEncoder().encodeToString(imageBytes); ContentBlockParam imageBlock = ContentBlockParam.ofImage( ImageBlockParam.builder() .source( Base64ImageSource.builder() .mediaType(Base64ImageSource.MediaType.IMAGE_JPEG) .data(imageBase64) .build() ) .build() ); ContentBlockParam textBlock = ContentBlockParam.ofText( TextBlockParam.builder().text("Describe this image").build() ); MessageCountTokensParams params = MessageCountTokensParams.builder() .model(Model.CLAUDE_OPUS_5) .addUserMessageOfBlockParams(List.of(imageBlock, textBlock)) .build(); MessageTokensCount count = client.messages().countTokens(params); System.out.println(count); ``` ```php PHP $imageUrl = "https://platform.claude.com/docs/images/vision-example.jpg"; $imageMediaType = "image/jpeg"; $imageData = base64_encode(file_get_contents($imageUrl)); $client = new Client(); $response = $client->messages->countTokens( messages: [ [ 'role' => 'user', 'content' => [ [ 'type' => 'image', 'source' => [ 'type' => 'base64', 'media_type' => $imageMediaType, 'data' => $imageData ] ], ['type' => 'text', 'text' => 'Describe this image'] ] ] ], model: 'claude-opus-5', ); print_r($response); ``` ```ruby Ruby require "base64" require "net/http" image_url = "https://platform.claude.com/docs/images/vision-example.jpg" image_media_type = "image/jpeg" uri = URI(image_url) image_data = Base64.strict_encode64(Net::HTTP.get(uri)) client = Anthropic::Client.new response = client.messages.count_tokens( model: "claude-opus-5", messages: [ { role: "user", content: [ { type: "image", source: { type: "base64", media_type: image_media_type, data: image_data } }, { type: "text", text: "Describe this image" } ] } ] ) puts response ``` ```json Output { "input_tokens": 1028 } ``` ### Count tokens in messages with thinking See [Thinking and the context window](https://platform.claude.com/docs/en/build-with-claude/thinking#thinking-and-the-context-window) for more details. * Thinking blocks from **previous** assistant turns are ignored and **do not** count toward your input tokens * **Current** assistant turn thinking **does** count toward your input tokens ```bash cURL curl https://api.anthropic.com/v1/messages/count_tokens \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "content-type: application/json" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-sonnet-4-6", "thinking": { "type": "enabled", "budget_tokens": 16000 }, "messages": [ { "role": "user", "content": "Are there an infinite number of prime numbers such that n mod 4 == 3?" }, { "role": "assistant", "content": [ { "type": "thinking", "thinking": "This is a nice number theory question. Lets think about it step by step...", "signature": "EuYBCkQYAiJAgCs1le6/Pol5Z4/JMomVOouGrWdhYNsH3ukzUECbB6iWrSQtsQuRHJID6lWV..." }, { "type": "text", "text": "Yes, there are infinitely many prime numbers p such that p mod 4 = 3..." } ] }, { "role": "user", "content": "Can you write a formal proof?" } ] }' ``` ```bash CLI ant messages count-tokens <<'YAML' model: claude-sonnet-4-6 thinking: type: enabled budget_tokens: 16000 messages: - role: user content: Are there an infinite number of prime numbers such that n mod 4 == 3? - role: assistant content: - type: thinking thinking: >- This is a nice number theory question. Lets think about it step by step... signature: >- EuYBCkQYAiJAgCs1le6/Pol5Z4/JMomVOouGrWdhYNsH3ukzUECbB6iWrSQtsQuRHJID6lWV... - type: text text: Yes, there are infinitely many prime numbers p such that p mod 4 = 3... - role: user content: Can you write a formal proof? YAML ``` ```python Python client = anthropic.Anthropic() response = client.messages.count_tokens( model="claude-sonnet-4-6", thinking={"type": "enabled", "budget_tokens": 16000}, messages=[ { "role": "user", "content": "Are there an infinite number of prime numbers such that n mod 4 == 3?", }, { "role": "assistant", "content": [ { "type": "thinking", "thinking": "This is a nice number theory question. Let's think about it step by step...", "signature": "EuYBCkQYAiJAgCs1le6/Pol5Z4/JMomVOouGrWdhYNsH3ukzUECbB6iWrSQtsQuRHJID6lWV...", }, { "type": "text", "text": "Yes, there are infinitely many prime numbers p such that p mod 4 = 3...", }, ], }, {"role": "user", "content": "Can you write a formal proof?"}, ], ) print(response.json()) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.messages.countTokens({ model: "claude-sonnet-4-6", thinking: { type: "enabled", budget_tokens: 16000 }, messages: [ { role: "user", content: "Are there an infinite number of prime numbers such that n mod 4 == 3?" }, { role: "assistant", content: [ { type: "thinking", thinking: "This is a nice number theory question. Let's think about it step by step...", signature: "EuYBCkQYAiJAgCs1le6/Pol5Z4/JMomVOouGrWdhYNsH3ukzUECbB6iWrSQtsQuRHJID6lWV..." }, { type: "text", text: "Yes, there are infinitely many prime numbers p such that p mod 4 = 3..." } ] }, { role: "user", content: "Can you write a formal proof?" } ] }); console.log(response); ``` ```csharp C# using System; using System.Threading.Tasks; using System.Collections.Generic; using Anthropic; using Anthropic.Models.Messages; AnthropicClient client = new(); var parameters = new MessageCountTokensParams { Model = Model.ClaudeSonnet4_6, Thinking = new ThinkingConfigEnabled(budgetTokens: 16000), Messages = [ new() { Role = Role.User, Content = "Are there an infinite number of prime numbers such that n mod 4 == 3?" }, new() { Role = Role.Assistant, Content = new MessageParamContent(new List { new ContentBlockParam(new ThinkingBlockParam() { Thinking = "This is a nice number theory question. Let's think about it step by step...", Signature = "EuYBCkQYAiJAgCs1le6/Pol5Z4/JMomVOouGrWdhYNsH3ukzUECbB6iWrSQtsQuRHJID6lWV...", }), new ContentBlockParam(new TextBlockParam("Yes, there are infinitely many prime numbers p such that p mod 4 = 3...")), }), }, new() { Role = Role.User, Content = "Can you write a formal proof?" } ] }; var response = await client.Messages.CountTokens(parameters); Console.WriteLine(response); ``` ```go Go client := anthropic.NewClient() thinkingBlock := anthropic.NewThinkingBlock( "EuYBCkQYAiJAgCs1le6/Pol5Z4/JMomVOouGrWdhYNsH3ukzUECbB6iWrSQtsQuRHJID6lWV...", "This is a nice number theory question. Let's think about it step by step...", ) textBlock := anthropic.NewTextBlock( "Yes, there are infinitely many prime numbers p such that p mod 4 = 3...", ) response, err := client.Messages.CountTokens(context.TODO(), anthropic.MessageCountTokensParams{ Model: anthropic.ModelClaudeSonnet4_6, Thinking: anthropic.ThinkingConfigParamOfEnabled(16000), Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Are there an infinite number of prime numbers such that n mod 4 == 3?")), anthropic.NewAssistantMessage(thinkingBlock, textBlock), anthropic.NewUserMessage(anthropic.NewTextBlock("Can you write a formal proof?")), }, }) if err != nil { log.Fatal(err) } fmt.Printf("%+v\n", response) ``` ```java Java import com.anthropic.models.messages.MessageCountTokensParams; import com.anthropic.models.messages.MessageTokensCount; // ... import com.anthropic.models.messages.ThinkingBlockParam; // ... AnthropicClient client = AnthropicOkHttpClient.fromEnv(); List assistantBlocks = List.of( ContentBlockParam.ofThinking( ThinkingBlockParam.builder() .thinking( "This is a nice number theory question. Let's think about it step by step..." ) .signature( "EuYBCkQYAiJAgCs1le6/Pol5Z4/JMomVOouGrWdhYNsH3ukzUECbB6iWrSQtsQuRHJID6lWV..." ) .build() ), ContentBlockParam.ofText( TextBlockParam.builder() .text("Yes, there are infinitely many prime numbers p such that p mod 4 = 3...") .build() ) ); MessageCountTokensParams params = MessageCountTokensParams.builder() .model(Model.CLAUDE_SONNET_4_6) .enabledThinking(16000) .addUserMessage("Are there an infinite number of prime numbers such that n mod 4 == 3?") .addAssistantMessageOfBlockParams(assistantBlocks) .addUserMessage("Can you write a formal proof?") .build(); MessageTokensCount count = client.messages().countTokens(params); System.out.println(count); ``` ```php PHP $client = new Client(); $response = $client->messages->countTokens( messages: [ [ 'role' => 'user', 'content' => 'Are there an infinite number of prime numbers such that n mod 4 == 3?' ], [ 'role' => 'assistant', 'content' => [ [ 'type' => 'thinking', 'thinking' => 'This is a nice number theory question. Let\'s think about it step by step...', 'signature' => 'EuYBCkQYAiJAgCs1le6/Pol5Z4/JMomVOouGrWdhYNsH3ukzUECbB6iWrSQtsQuRHJID6lWV...' ], [ 'type' => 'text', 'text' => 'Yes, there are infinitely many prime numbers p such that p mod 4 = 3...' ] ] ], [ 'role' => 'user', 'content' => 'Can you write a formal proof?' ] ], model: 'claude-sonnet-4-6', thinking: [ 'type' => 'enabled', 'budget_tokens' => 16000 ], ); echo json_encode($response); ``` ```ruby Ruby client = Anthropic::Client.new response = client.messages.count_tokens( model: "claude-sonnet-4-6", thinking: { type: "enabled", budget_tokens: 16000 }, messages: [ { role: "user", content: "Are there an infinite number of prime numbers such that n mod 4 == 3?" }, { role: "assistant", content: [ { type: "thinking", thinking: "This is a nice number theory question. Let's think about it step by step...", signature: "EuYBCkQYAiJAgCs1le6/Pol5Z4/JMomVOouGrWdhYNsH3ukzUECbB6iWrSQtsQuRHJID6lWV..." }, { type: "text", text: "Yes, there are infinitely many prime numbers p such that p mod 4 = 3..." } ] }, { role: "user", content: "Can you write a formal proof?" } ] ) puts response ``` ```json Output { "input_tokens": 88 } ``` ### Count tokens in messages with PDFs Token counting supports PDFs with the same [PDF support limitations](https://platform.claude.com/docs/en/build-with-claude/pdf-support#pdf-support-limitations) as the Messages API. ```bash cURL curl https://api.anthropic.com/v1/messages/count_tokens \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "content-type: application/json" \ -H "anthropic-version: 2023-06-01" \ -d @- < { new ContentBlockParam(new DocumentBlockParam( new DocumentBlockParamSource(new Base64PdfSource() { Data = pdfBase64, }) )), new ContentBlockParam(new TextBlockParam("Please summarize this document.")), }), } ] }; var count = await client.Messages.CountTokens(parameters); Console.WriteLine(count); ``` ```go Go client := anthropic.NewClient() pdfBytes, err := os.ReadFile("/path/to/document.pdf") if err != nil { log.Fatal(err) } pdfBase64 := base64.StdEncoding.EncodeToString(pdfBytes) response, err := client.Messages.CountTokens(context.TODO(), anthropic.MessageCountTokensParams{ Model: anthropic.ModelClaudeOpus5, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage( anthropic.NewDocumentBlock(anthropic.Base64PDFSourceParam{ Data: pdfBase64, }), anthropic.NewTextBlock("Please summarize this document."), ), }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java import com.anthropic.models.messages.Base64PdfSource; // ... import com.anthropic.models.messages.DocumentBlockParam; import com.anthropic.models.messages.MessageCountTokensParams; import com.anthropic.models.messages.MessageTokensCount; // ... AnthropicClient client = AnthropicOkHttpClient.fromEnv(); byte[] fileBytes = Files.readAllBytes(Path.of("/path/to/document.pdf")); String pdfBase64 = Base64.getEncoder().encodeToString(fileBytes); ContentBlockParam documentBlock = ContentBlockParam.ofDocument( DocumentBlockParam.builder() .source(Base64PdfSource.builder().data(pdfBase64).build()) .build() ); ContentBlockParam textBlock = ContentBlockParam.ofText( TextBlockParam.builder().text("Please summarize this document.").build() ); MessageCountTokensParams params = MessageCountTokensParams.builder() .model(Model.CLAUDE_OPUS_5) .addUserMessageOfBlockParams(List.of(documentBlock, textBlock)) .build(); MessageTokensCount count = client.messages().countTokens(params); System.out.println(count); ``` ```php PHP $client = new Client(); $pdfBase64 = base64_encode(file_get_contents("/path/to/document.pdf")); $response = $client->messages->countTokens( messages: [ [ 'role' => 'user', 'content' => [ [ 'type' => 'document', 'source' => [ 'type' => 'base64', 'media_type' => 'application/pdf', 'data' => $pdfBase64 ] ], [ 'type' => 'text', 'text' => 'Please summarize this document.' ] ] ] ], model: 'claude-opus-5', ); echo json_encode($response); ``` ```ruby Ruby require "base64" client = Anthropic::Client.new pdf_base64 = Base64.strict_encode64(File.binread("/path/to/document.pdf")) response = client.messages.count_tokens( model: "claude-opus-5", messages: [ { role: "user", content: [ { type: "document", source: { type: "base64", media_type: "application/pdf", data: pdf_base64 } }, { type: "text", text: "Please summarize this document." } ] } ] ) puts response ``` ```json Output { "input_tokens": 2188 } ``` *** ## Token counts on Claude Fable 5 and Claude Mythos 5 Claude Fable 5 and Claude Mythos 5 use the tokenizer introduced with Claude Opus 4.7, which produces roughly 30 percent more tokens than models before Claude Opus 4.7 for the same text. The exact increase depends on the content and workload shape. The token counting endpoint returns the count under the tokenizer of the `model` you pass, so to measure the difference for your workload, count the same request twice: once with your current model and once with `model: "claude-fable-5"` (or `"claude-mythos-5"`), and compare the two `input_tokens` values. **Billing and migration:** Usage and billing on Claude Fable 5 and Claude Mythos 5 reflect this tokenizer's counts. If you're migrating from a model before Claude Opus 4.7, the same content consumes roughly 30 percent more tokens. The exact increase depends on the content and workload shape. When migrating a workload to Claude Fable 5 and Claude Mythos 5, don't reuse token counts measured on a model before Claude Opus 4.7 to estimate costs or context window fit. Count your prompts with `model: "claude-fable-5"` (or `"claude-mythos-5"`). *** ## Pricing and rate limits Token counting is **free to use** but subject to requests per minute rate limits based on your [usage tier](https://platform.claude.com/docs/en/api/rate-limits#rate-limits). If you need higher limits, use **Request rate limit increase** on the [Rate limits](https://platform.claude.com/settings/limits) page. | Usage tier | Requests per minute (RPM) | | ---------- | ------------------------- | | Start | 2,000 | | Build | 4,000 | | Scale | 8,000 | Token counting and message creation have separate and independent rate limits. Usage of one does not count against the limits of the other. *** ## FAQ No, token counting provides an estimate without using caching logic. Although you may provide `cache_control` blocks in your token counting request, prompt caching only occurs during actual message creation. *** ## Next steps Read the full API reference for the token counting endpoint. Use token counts to keep prompts within a model's context window. Check token counts before you send a request to stay within your usage tier. Reduce cost and latency on repeated prompts by caching prompt prefixes. ### Working with files --- title: Files API url: https://platform.claude.com/docs/en/build-with-claude/files description: Upload files once, reference them by file_id in Messages requests, and download outputs created by skills or the code execution tool. --- ## Compatibility - Status: Beta - [Beta header](https://platform.claude.com/docs/en/api/beta-headers): `files-api-2025-04-14` - [ZDR](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention): not eligible - Platforms: Claude API (beta), Claude Platform on AWS (beta), Microsoft Foundry (beta) [1]; not available on Amazon Bedrock, Google Cloud 1. On [Microsoft Foundry](https://platform.claude.com/docs/en/build-with-claude/claude-in-microsoft-foundry), the Files API requires a [Hosted on Anthropic deployment](https://platform.claude.com/docs/en/build-with-claude/claude-in-microsoft-foundry#additional-features-not-supported-when-hosted-on-azure). The Files API lets you upload and manage files to use with the Claude API without re-uploading content with each request. This is particularly useful when using the [code execution tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool) to provide inputs (for example, datasets and documents) and then download outputs (for example, charts). You can [explore the API reference directly](https://platform.claude.com/docs/en/api/beta/files/upload), in addition to this guide. Reach out through the [feedback form](https://forms.gle/tisHyierGwgN4DUE9) to share your experience with the Files API. ## File type support Referencing a `file_id` in a Messages request is supported on all models that support the given file type. [Images](https://platform.claude.com/docs/en/build-with-claude/vision) are supported on all current Claude models. For [PDFs](https://platform.claude.com/docs/en/build-with-claude/pdf-support) and [other file types with the code execution tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool#model-compatibility), see the linked pages for model support. ## How the Files API works The Files API provides a create-once, use-many-times approach for working with files: * **Upload files** to Anthropic's secure storage and receive a unique `file_id` * **Download files** that are created by skills or the code execution tool * **Reference files** in [Messages](https://platform.claude.com/docs/en/api/messages/create) requests using the `file_id` instead of re-uploading content * **Manage your files** with list, retrieve, and delete operations **Uploaded files are accessible to your entire workspace, not scoped to an end user, conversation, or session.** Any API key in the same workspace can access any file uploaded there, and all of your keys share your organization's Default Workspace unless you have assigned them to separate [workspaces](https://platform.claude.com/docs/en/manage-claude/workspaces#api-keys-and-resource-scoping). Never accept `file_id` values from end users or other untrusted sources: a user-supplied file ID would let one user of your application read content that another user uploaded. Treat file IDs as server-side references, and keep the mapping between your users and their files in your application. ## How to use the Files API To use the Files API, you'll need to include the beta feature header: `anthropic-beta: files-api-2025-04-14`. The SDKs add this header automatically when you call methods on the `beta.files` namespace, so the SDK examples on this page don't pass it explicitly for file operations. Messages requests that reference a file do need it, which the SDK examples pass through their `betas` parameter. ### Uploading a file Upload a file to be referenced in future API calls: ```bash cURL FILE_ID=$(curl -X POST https://api.anthropic.com/v1/files \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: files-api-2025-04-14" \ -F "file=@/path/to/document.pdf" | jq -r '.id') echo "$FILE_ID" ``` ```bash CLI FILE_ID=$(ant beta:files upload \ --file /path/to/document.pdf \ --transform id \ --raw-output) echo "$FILE_ID" ``` ```python Python uploaded = client.beta.files.upload( file=("document.pdf", open("/path/to/document.pdf", "rb"), "application/pdf"), ) file_id = uploaded.id print(file_id) ``` ```typescript TypeScript const uploaded = await client.beta.files.upload({ file: await toFile( fs.createReadStream("/path/to/document.pdf"), undefined, { type: "application/pdf" }, ), }); console.log(uploaded.id); ``` ```csharp C# var uploaded = await client.Beta.Files.Upload( new FileUploadParams { File = new BinaryContent { Stream = File.OpenRead("/path/to/document.pdf"), FileName = "document.pdf", ContentType = new("application/pdf") } }); var fileId = uploaded.ID; Console.WriteLine(fileId); ``` ```go Go f, err := os.Open("/path/to/document.pdf") if err != nil { log.Fatal(err) } defer f.Close() response, err := client.Beta.Files.Upload(context.Background(), anthropic.BetaFileUploadParams{ File: anthropic.File(f, "document.pdf", "application/pdf"), }) if err != nil { log.Fatal(err) } fileID := response.ID fmt.Println(fileID) ``` ```java Java FileMetadata file = client.beta().files().upload( FileUploadParams.builder() .file(MultipartField.builder() .value(Files.newInputStream(Path.of("/path/to/document.pdf"))) .filename("document.pdf") .contentType("application/pdf") .build()) .build() ); String fileId = file.id(); System.out.println(fileId); ``` ```php PHP $file = $client->beta->files->upload( FileParam::fromResource(fopen('/path/to/document.pdf', 'rb'), contentType: 'application/pdf'), ); $fileId = $file->id; echo $fileId; ``` ```ruby Ruby file = client.beta.files.upload( file: Anthropic::FilePart.new( Pathname("/path/to/document.pdf"), content_type: "application/pdf" ) ) file_id = file.id puts file_id ``` The response from uploading a file includes: ```json Response { "id": "file_011CNha8iCJcU1wXNR6q4V8w", "type": "file", "filename": "document.pdf", "mime_type": "application/pdf", "size_bytes": 1024000, "created_at": "2025-01-01T00:00:00Z", "downloadable": false } ``` `downloadable` is `false` for files you upload. Only files created by [skills](https://platform.claude.com/docs/en/build-with-claude/skills-guide) or the [code execution tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool) can be downloaded. See [Downloading a file](https://platform.claude.com/docs/en/build-with-claude/files#downloading-a-file). ### Using a file in messages Once uploaded, reference the file by passing the `id` from the upload response as `file_id`: ```bash cURL curl -X POST https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: files-api-2025-04-14" \ -H "content-type: application/json" \ -d @- < { new BetaTextBlockParam { Text = "Please summarize this document for me." }, new BetaRequestDocumentBlock { Source = new BetaFileDocumentSource { FileID = fileId } } } } ] }); Console.WriteLine(response); ``` ```go Go msg, err := client.Beta.Messages.New(context.Background(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Betas: []anthropic.AnthropicBeta{anthropic.AnthropicBetaFilesAPI2025_04_14}, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage( anthropic.NewBetaTextBlock("Please summarize this document for me."), anthropic.NewBetaDocumentBlock(anthropic.BetaFileDocumentSourceParam{ FileID: fileID, }), ), }, }) if err != nil { log.Fatal(err) } fmt.Println(msg) ``` ```java Java MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .addBeta("files-api-2025-04-14") .maxTokens(1024) .addUserMessageOfBetaContentBlockParams(List.of( BetaContentBlockParam.ofText(BetaTextBlockParam.builder() .text("Please summarize this document for me.") .build()), BetaContentBlockParam.ofDocument(BetaRequestDocumentBlock.builder() .source(BetaFileDocumentSource.builder() .fileId(fileId) .build()) .build()) )) .build(); BetaMessage message = client.beta().messages().create(params); System.out.println(message); ``` ```php PHP $response = $client->beta->messages->create( maxTokens: 1024, messages: [ [ 'role' => 'user', 'content' => [ ['type' => 'text', 'text' => 'Please summarize this document for me.'], [ 'type' => 'document', 'source' => [ 'type' => 'file', 'file_id' => $fileId ] ] ] ] ], model: 'claude-opus-5', betas: ['files-api-2025-04-14'], ); print_r($response); ``` ```ruby Ruby response = client.beta.messages.create( model: "claude-opus-5", max_tokens: 1024, betas: ["files-api-2025-04-14"], messages: [ { role: "user", content: [ { type: "text", text: "Please summarize this document for me." }, { type: "document", source: { type: "file", file_id: file_id } } ] } ] ) puts response ``` ### File types and content blocks The Files API supports different file types that correspond to different content block types: | File type | MIME type | Content block type | Use case | | --------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | ------------------ | ----------------------------------- | | PDF | `application/pdf` | `document` | Text analysis, document processing | | Plain text | `text/plain` | `document` | Text analysis, processing | | Images | `image/jpeg`, `image/png`, `image/gif`, `image/webp` | `image` | Image analysis, visual tasks | | [Datasets, others](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool#upload-and-analyze-your-own-files) | Varies | `container_upload` | Analyze data, create visualizations | #### Document blocks For PDFs and text files, use the `document` content block: ```json { "type": "document", "source": { "type": "file", "file_id": "file_011CNha8iCJcU1wXNR6q4V8w" }, "title": "Document Title", // Optional "context": "Context about the document", // Optional "citations": { "enabled": true } // Optional, enables citations } ``` #### Image blocks For images, use the `image` content block: ```json { "type": "image", "source": { "type": "file", "file_id": "file_011CPMxVD3fHLUhvTqtsQA5w" } } ``` #### Container upload blocks To send a file to the [code execution tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool#upload-and-analyze-your-own-files), use the `container_upload` content block: ```json { "type": "container_upload", "file_id": "file_011CNha8iCJcU1wXNR6q4V8w" } ``` ### Working with other file formats For file types that the `document` block doesn't support (for example, .docx and .xlsx), convert the files to plain text and include the content directly in your message. Files that are already plain text, such as .csv and .md files, can either be read in this way or uploaded through the Files API with an explicit `text/plain` content type. To analyze datasets instead of reading them as text, upload them for the [code execution tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool#upload-and-analyze-your-own-files) using a `container_upload` block. The following examples read a text file and send its contents as plain text: ```bash cURL # Read the text file # Note: For files with special characters, consider base64 encoding TEXT_CONTENT=$(cat document.txt) curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d @- < block.type === "text" ); console.log(textBlock?.text); ``` ```csharp C# AnthropicClient client = new(); // Read the text file string textContent = await File.ReadAllTextAsync("document.txt"); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [new() { Role = Role.User, Content = $"Here's the document content:\n\n{textContent}\n\nPlease summarize this document." }] }; var message = await client.Messages.Create(parameters); foreach (var block in message.Content) { if (block.TryPickText(out var textBlock)) { Console.WriteLine(textBlock.Text); } } ``` ```go Go client := anthropic.NewClient() // Read the text file textContent, err := os.ReadFile("document.txt") if err != nil { log.Fatal(err) } response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock( fmt.Sprintf("Here's the document content:\n\n%s\n\nPlease summarize this document.", string(textContent)), )), }, }) if err != nil { log.Fatal(err) } for _, block := range response.Content { if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok { fmt.Println(textBlock.Text) } } ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); // Read the text file String textContent = Files.readString(Path.of("document.txt")); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addUserMessage("Here's the document content:\n\n" + textContent + "\n\nPlease summarize this document.") .build(); Message response = client.messages().create(params); response.content().stream() .flatMap(block -> block.text().stream()) .forEach(textBlock -> System.out.println(textBlock.text())); ``` ```php PHP $client = new Client(); // Read the text file $textContent = file_get_contents("document.txt"); $message = $client->messages->create( maxTokens: 1024, messages: [ [ 'role' => 'user', 'content' => [ [ 'type' => 'text', 'text' => "Here's the document content:\n\n{$textContent}\n\nPlease summarize this document." ] ] ] ], model: 'claude-opus-5', ); foreach ($message->content as $block) { if ($block->type === 'text') { echo $block->text, PHP_EOL; } } ``` ```ruby Ruby client = Anthropic::Client.new # Read the text file text_content = File.read("document.txt") message = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: [ { type: "text", text: "Here's the document content:\n\n#{text_content}\n\nPlease summarize this document." } ] } ] ) message.content.each do |block| puts block.text if block.type == :text end ``` For .docx files containing images, convert them to PDF format first, then use [PDF support](https://platform.claude.com/docs/en/build-with-claude/pdf-support) to take advantage of the built-in image parsing. This allows using citations from the PDF document. ### Managing files #### List files Retrieve a list of your uploaded files. The endpoint is paginated: each request returns up to `limit` files (20 by default), and the `before_id` and `after_id` parameters fetch the adjacent page. See the [List Files API reference](https://platform.claude.com/docs/en/api/beta/files/list). The SDKs return the first page and provide auto-pagination helpers. The CLI example bounds the total with `--max-items`: ```bash cURL curl https://api.anthropic.com/v1/files \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: files-api-2025-04-14" ``` ```bash CLI ant beta:files list \ --max-items 10 ``` ```python Python client = anthropic.Anthropic() files = client.beta.files.list() print(files) ``` ```typescript TypeScript const client = new Anthropic(); const files = await client.beta.files.list(); console.log(files); ``` ```csharp C# AnthropicClient client = new(); var files = await client.Beta.Files.List(); Console.WriteLine(files); ``` ```go Go client := anthropic.NewClient() files, err := client.Beta.Files.List(context.TODO(), anthropic.BetaFileListParams{}) if err != nil { log.Fatal(err) } fmt.Println(files) ``` ```java Java import com.anthropic.models.beta.files.FileListPage; // ... void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); FileListPage files = client.beta().files().list(); System.out.println(files); } ``` ```php PHP $client = new Client(); $files = $client->beta->files->list(); echo $files; ``` ```ruby Ruby client = Anthropic::Client.new files = client.beta.files.list puts files ``` #### Get file metadata Retrieve information about a specific file: ```bash cURL curl "https://api.anthropic.com/v1/files/$FILE_ID" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: files-api-2025-04-14" ``` ```bash CLI ant beta:files retrieve-metadata \ --file-id "$FILE_ID" ``` ```python Python file = client.beta.files.retrieve_metadata(file_id) print(file) ``` ```typescript TypeScript const file = await client.beta.files.retrieveMetadata(uploaded.id); console.log(file); ``` ```csharp C# var file = await client.Beta.Files.RetrieveMetadata(fileId); Console.WriteLine(file); ``` ```go Go metadata, err := client.Beta.Files.GetMetadata( context.TODO(), fileID, anthropic.BetaFileGetMetadataParams{}, ) if err != nil { log.Fatal(err) } fmt.Println(metadata) ``` ```java Java FileMetadata metadata = client.beta().files().retrieveMetadata(fileId); System.out.println(metadata); ``` ```php PHP $file = $client->beta->files->retrieveMetadata($fileId); echo $file; ``` ```ruby Ruby file = client.beta.files.retrieve_metadata(file_id) puts file ``` #### Delete a file Remove a file from your workspace: ```bash cURL curl -X DELETE "https://api.anthropic.com/v1/files/$FILE_ID" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: files-api-2025-04-14" ``` ```bash CLI ant beta:files delete \ --file-id "$FILE_ID" ``` ```python Python client.beta.files.delete(file_id) ``` ```typescript TypeScript await client.beta.files.delete(uploaded.id); ``` ```csharp C# await client.Beta.Files.Delete(fileId); ``` ```go Go _, err = client.Beta.Files.Delete( context.TODO(), fileID, anthropic.BetaFileDeleteParams{}, ) if err != nil { log.Fatal(err) } ``` ```java Java client.beta().files().delete(fileId); ``` ```php PHP $client->beta->files->delete($fileId); ``` ```ruby Ruby client.beta.files.delete(file_id) ``` ### Downloading a file Download files that were created by [skills](https://platform.claude.com/docs/en/build-with-claude/skills-guide) or the [code execution tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool). Files you upload cannot be downloaded. The `file_id` of a generated file appears in the [`bash_code_execution_tool_result` content block](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool#retrieve-generated-files) of the Messages response that created it: ```bash cURL curl -X GET "https://api.anthropic.com/v1/files/$FILE_ID/content" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: files-api-2025-04-14" \ --output downloaded_file.txt ``` ```bash CLI ant beta:files download \ --file-id "$FILE_ID" \ --output downloaded_file.txt ``` ```python Python file_content = client.beta.files.download(file_id) file_content.write_to_file("downloaded_file.txt") ``` ```typescript TypeScript const content = await client.beta.files.download(uploaded.id); const bytes = Buffer.from(await content.arrayBuffer()); await fsp.writeFile("downloaded_file.txt", bytes); ``` ```csharp C# using var fileContent = await client.Beta.Files.Download(fileId); await using var source = await fileContent.ReadAsStream(); await using var destination = File.Create("downloaded_file.txt"); await source.CopyToAsync(destination); ``` ```go Go func downloadFile(client anthropic.Client, fileID string) error { resp, err := client.Beta.Files.Download( context.TODO(), fileID, anthropic.BetaFileDownloadParams{}, ) if err != nil { return err } defer resp.Body.Close() out, err := os.Create("downloaded_file.txt") if err != nil { return err } defer out.Close() _, err = io.Copy(out, resp.Body) return err } ``` ```java Java try (HttpResponse response = client.beta().files().download(fileId)) { try (InputStream body = response.body()) { Files.copy(body, Path.of("downloaded_file.txt"), StandardCopyOption.REPLACE_EXISTING); } } ``` ```php PHP $fileContent = $client->beta->files->download($fileId); file_put_contents("downloaded_file.txt", $fileContent); ``` ```ruby Ruby file_content = client.beta.files.download(file_id) File.binwrite("downloaded_file.txt", file_content.read) ``` A file is downloadable only when its metadata shows `"downloadable": true`, which is the case for files created by skills or the code execution tool. Downloading a file you uploaded returns a 400 error. ## File storage and limits ### Storage limits * **Maximum file size:** 500 MB per file * **Total storage:** 500 GB per organization ### File lifecycle * Files are scoped to the workspace of the API key that uploaded them. Any API key in the same workspace can reference them; never accept file IDs from untrusted sources (see the [workspace access warning](https://platform.claude.com/docs/en/build-with-claude/files#workspace-scoped-access)) * Files cannot be modified or renamed after upload. To change a file's content, upload a new file and delete the old one * Files persist until you delete them with the `DELETE /v1/files/{file_id}` endpoint * Deleted files cannot be recovered * Files are inaccessible through the API shortly after deletion, but they may persist in active Messages API calls and associated tool uses * Files that users delete will be deleted in accordance with Anthropic's [data retention policy](https://privacy.claude.com/en/articles/7996866-how-long-do-you-store-my-organization-s-data). For ZDR eligibility across all features, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention) ## Error handling Common errors when using the Files API include: * **File not found (404):** The specified `file_id` doesn't exist or you don't have access to it * **Invalid file type (400):** The file type doesn't match the content block type (for example, using an image file in a document block) * **Not downloadable (400):** Files you upload have `"downloadable": false` and cannot be downloaded. Only files created by skills or the code execution tool can be downloaded * **Exceeds context window size (400):** The file is larger than the context window size (for example, using a 500 MB plain text file in a `/v1/messages` request) * **Invalid filename (400):** The file name doesn't meet the length requirements (1-255 characters) or contains forbidden characters (`<`, `>`, `:`, `"`, `|`, `?`, `*`, `\`, `/`, or Unicode characters 0-31) * **File too large (413):** File exceeds the 500 MB limit * **Storage limit exceeded (400):** Your organization has reached the 500 GB storage limit ```json Output { "type": "error", "error": { "type": "not_found_error", "message": "File `file_011CNha8iCJcU1wXNR6q4V8w` not found." }, "request_id": "req_011CQFYcrRp7mCHLDsAYT8Qt" } ``` ## Usage and billing Files API operations are free: * Uploading files * Downloading files * Listing files * Getting file metadata * Deleting files File content used in Messages requests is priced as input tokens. ### Rate limits During the beta period: * File-related API calls are limited to approximately 100 requests per minute * [Contact us](mailto:sales@anthropic.com) if you need higher limits for your use case ## Next steps Process PDFs with Claude. Extract text, analyze charts, and understand visual content from your documents. Run Python and bash code in a sandboxed container to analyze data, generate files, and iterate on solutions. Process and analyze visual input and generate text and code from images. --- title: PDF support url: https://platform.claude.com/docs/en/build-with-claude/pdf-support description: "Process PDFs with Claude: extract text, analyze charts, and understand visual content from your documents." --- ## Compatibility - [ZDR](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention): eligible (excludes [Covered Models](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#model-specific-data-retention-requirements)) - Platforms: Claude API, Claude Platform on AWS, Amazon Bedrock, Google Cloud, Microsoft Foundry You can ask Claude about any text, pictures, charts, and tables in PDFs you provide. Some sample use cases: * Analyzing financial reports and understanding charts/tables * Extracting key information from legal documents * Assisting with document translation * Converting document information into structured formats ## Before you begin ### Check PDF requirements Claude works with any standard PDF. Ensure your request size meets these requirements: | Requirement | Limit | | ------------------------- | -------------------------------------------------------------------------------------------------- | | Maximum request size | 32 MB ([varies by platform](https://platform.claude.com/docs/en/api/overview#request-size-limits)) | | Maximum pages per request | 600 (100 when the request's context window is under 1M tokens) | | Format | Standard PDF (no passwords/encryption) | Both limits are on the entire request payload, including any other content sent alongside PDFs. For large PDFs, consider uploading with the [Files API](https://platform.claude.com/docs/en/build-with-claude/files) and referencing by `file_id` to keep request payloads small. Dense PDFs (many small-font pages, complex tables, or heavy graphics) can fill the context window before reaching the page limit. Requests with large PDFs can also fail before reaching the page limit, even when using the Files API. Try splitting the document into sections; for large files, because each page is processed as an image, downsampling embedded images can also help. Because PDF support relies on Claude's vision capabilities, it is subject to the same [limitations and considerations](https://platform.claude.com/docs/en/build-with-claude/vision#limitations) as other vision tasks. ### Supported platforms and models All [active models](https://platform.claude.com/docs/en/about-claude/models/overview) support PDF processing. For PDF support through Amazon Bedrock's Converse API, see [Amazon Bedrock PDF support](https://platform.claude.com/docs/en/build-with-claude/pdf-support#amazon-bedrock-pdf-support). ### Amazon Bedrock PDF support When using PDF support through the Converse API, part of [Claude on Amazon Bedrock (Opus 4.6 and earlier)](https://platform.claude.com/docs/en/build-with-claude/claude-on-amazon-bedrock-legacy), there are two distinct document processing modes: **Important:** To access Claude's full visual PDF understanding capabilities in the Converse API, you must enable citations. Without citations enabled, the API falls back to basic text extraction only. Learn more about [working with citations](https://platform.claude.com/docs/en/build-with-claude/citations). #### Document processing modes 1. **Converse Document Chat** (Original mode - Text extraction only) * Provides basic text extraction from PDFs * Cannot analyze images, charts, or visual layouts within PDFs * Uses approximately 1,000 tokens for a 3-page PDF * Automatically used when citations are not enabled 2. **Claude PDF Chat** (New mode - Full visual understanding) * Provides complete visual analysis of PDFs * Can understand and analyze charts, graphs, images, and visual layouts * Processes each page as both text and image for comprehensive understanding * Uses approximately 7,000 tokens for a 3-page PDF * **Requires citations to be enabled** in the Converse API #### Key limitations * **Converse API:** Visual PDF analysis requires citations to be enabled. There is currently no option to use visual analysis without citations (unlike the InvokeModel API). * **InvokeModel API:** Provides full control over PDF processing without forced citations. #### Common issues If Claude isn't seeing images or charts in your PDFs when using the Converse API, you likely need to enable the citations flag. Without it, Converse falls back to basic text extraction only. This is a known constraint with the Converse API. For applications that require visual PDF analysis without citations, consider using the InvokeModel API instead. Plain text files such as .txt, .csv, or .md can be used directly in document blocks: upload them to the Files API with MIME type `text/plain` and reference them by `file_id`. Binary formats such as .xlsx or .docx are not supported in document blocks and must be converted to text or PDF first. See [Working with other file formats](https://platform.claude.com/docs/en/build-with-claude/files#working-with-other-file-formats). ## Process PDFs with Claude ### Send your first PDF request Start with a simple example using the Messages API. You can provide PDFs to Claude in three ways: 1. As a URL reference to a PDF hosted online 2. As a base64-encoded PDF in `document` content blocks 3. By a `file_id` from the [Files API](https://platform.claude.com/docs/en/build-with-claude/files) On Amazon Bedrock and Google Cloud, only base64-encoded sources are currently available. On Microsoft Foundry, the Files API is not supported for deployments hosted on Azure. #### Option 1: URL-based PDF document The simplest approach is to reference a PDF directly from a URL: ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "messages": [{ "role": "user", "content": [{ "type": "document", "source": { "type": "url", "url": "https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf" } }, { "type": "text", "text": "What are the key findings in this document?" }] }] }' ``` ```bash CLI ant messages create --transform content --format yaml <<'YAML' model: claude-opus-5 max_tokens: 1024 messages: - role: user content: - type: document source: type: url url: https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf - type: text text: What are the key findings in this document? YAML ``` ```python Python client = anthropic.Anthropic() message = client.messages.create( model="claude-opus-5", max_tokens=1024, messages=[ { "role": "user", "content": [ { "type": "document", "source": { "type": "url", "url": "https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf", }, }, {"type": "text", "text": "What are the key findings in this document?"}, ], } ], ) print(message.content) ``` ```typescript TypeScript const anthropic = new Anthropic(); const response = await anthropic.messages.create({ model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: [ { type: "document", source: { type: "url", url: "https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf" } }, { type: "text", text: "What are the key findings in this document?" } ] } ] }); console.log(response); ``` ```csharp C# var client = new AnthropicClient(); // Create document block with URL var documentParam = new DocumentBlockParam { Source = new UrlPdfSource { Url = "https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf", }, }; // Create a message with document and text content blocks var message = await client.Messages.Create(new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [ new() { Role = Role.User, Content = new List { documentParam, new TextBlockParam("What are the key findings in this document?"), }, }, ], }); Console.WriteLine(string.Join("\n", message.Content)); ``` ```go Go client := anthropic.NewClient() message, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage( anthropic.NewDocumentBlock(anthropic.URLPDFSourceParam{ URL: "https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf", }), anthropic.NewTextBlock("What are the key findings in this document?"), ), }, }) if err != nil { panic(err) } fmt.Printf("%+v\n", message.Content) ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); // Create document block with URL DocumentBlockParam documentParam = DocumentBlockParam.builder() .source( UrlPdfSource.builder() .url( "https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf" ) .build() ) .build(); // Create a message with document and text content blocks MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .addUserMessageOfBlockParams( List.of( ContentBlockParam.ofDocument(documentParam), ContentBlockParam.ofText( TextBlockParam.builder() .text("What are the key findings in this document?") .build() ) ) ) .build(); Message message = client.messages().create(params); System.out.println(message.content()); ``` ```php PHP $client = new Client(); $message = $client->messages->create( maxTokens: 1024, messages: [ [ 'role' => 'user', 'content' => [ [ 'type' => 'document', 'source' => [ 'type' => 'url', 'url' => 'https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf', ], ], [ 'type' => 'text', 'text' => 'What are the key findings in this document?', ], ], ], ], model: 'claude-opus-5', ); echo $message; ``` ```ruby Ruby anthropic = Anthropic::Client.new message = anthropic.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: [ { type: "document", source: { type: "url", url: "https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf" } }, {type: "text", text: "What are the key findings in this document?"} ] } ] ) puts(message.content) ``` The response returns Claude's analysis as text blocks in `content`, with token consumption in `usage`: ```json Output { "id": "msg_01Hfp8YuFjQ55VgWbpdHDehB", "type": "message", "role": "assistant", "model": "claude-opus-5", "content": [ { "type": "text", "text": "This document is an addendum to the Claude 3 model card, reporting updated evaluation results. The key findings include..." } ], "stop_reason": "end_turn", "usage": { "input_tokens": 45000, "output_tokens": 300 } } ``` #### Option 2: Base64-encoded PDF document If you need to send PDFs from your local system or when a URL isn't available: ```bash cURL # Method 1: Fetch and encode a remote PDF curl -sL "https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf" | base64 | tr -d '\n' > pdf_base64.txt # Method 2: Encode a local PDF file # base64 document.pdf | tr -d '\n' > pdf_base64.txt # Create a JSON request file using the pdf_base64.txt content jq -n --rawfile PDF_BASE64 pdf_base64.txt '{ "model": "claude-opus-5", "max_tokens": 1024, "messages": [{ "role": "user", "content": [{ "type": "document", "source": { "type": "base64", "media_type": "application/pdf", "data": $PDF_BASE64 } }, { "type": "text", "text": "What are the key findings in this document?" }] }] }' > request.json # Send the API request using the JSON file curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d @request.json ``` ```bash CLI ant messages create \ --model claude-opus-5 \ --max-tokens 1024 \ --transform content \ --format yaml <<'YAML' messages: - role: user content: - type: document source: type: base64 media_type: application/pdf data: "@./document.pdf" - type: text text: What are the key findings in this document? YAML ``` ```python Python import base64 import httpx # First, load and encode the PDF pdf_url = "https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf" pdf_data = base64.standard_b64encode( httpx.get(pdf_url, follow_redirects=True).content ).decode("utf-8") # Alternative: Load from a local file # with open("document.pdf", "rb") as f: # pdf_data = base64.standard_b64encode(f.read()).decode("utf-8") # Send to Claude using base64 encoding client = anthropic.Anthropic() message = client.messages.create( model="claude-opus-5", max_tokens=1024, messages=[ { "role": "user", "content": [ { "type": "document", "source": { "type": "base64", "media_type": "application/pdf", "data": pdf_data, }, }, {"type": "text", "text": "What are the key findings in this document?"}, ], } ], ) print(message.content) ``` ```typescript TypeScript // Method 1: Fetch and encode a remote PDF const pdfURL = "https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf"; const pdfResponse = await fetch(pdfURL); const arrayBuffer = await pdfResponse.arrayBuffer(); const pdfBase64 = Buffer.from(arrayBuffer).toString("base64"); // Method 2: Load from a local file // import { readFile } from "node:fs/promises"; // const pdfBase64 = (await readFile('document.pdf')).toString('base64'); // Send the API request with base64-encoded PDF const anthropic = new Anthropic(); const response = await anthropic.messages.create({ model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: [ { type: "document", source: { type: "base64", media_type: "application/pdf", data: pdfBase64 } }, { type: "text", text: "What are the key findings in this document?" } ] } ] }); console.log(response); ``` ```csharp C# var client = new AnthropicClient(); // Method 1: Download and encode a remote PDF var pdfUrl = "https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf"; using var httpClient = new HttpClient(); var pdfBase64 = Convert.ToBase64String(await httpClient.GetByteArrayAsync(pdfUrl)); // Method 2: Load from a local file // var pdfBase64 = Convert.ToBase64String(await File.ReadAllBytesAsync("document.pdf")); // Create document block with base64 data var documentParam = new DocumentBlockParam { Source = new Base64PdfSource { Data = pdfBase64 }, }; // Create a message with document and text content blocks var message = await client.Messages.Create(new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [ new() { Role = Role.User, Content = new List { documentParam, new TextBlockParam("What are the key findings in this document?"), }, }, ], }); Console.WriteLine(string.Join("\n", message.Content)); ``` ```go Go // First, load and encode the PDF pdfURL := "https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf" resp, err := http.Get(pdfURL) if err != nil { panic(err) } defer resp.Body.Close() pdfBytes, err := io.ReadAll(resp.Body) if err != nil { panic(err) } pdfBase64 := base64.StdEncoding.EncodeToString(pdfBytes) // Alternative: Load from a local file (add "os" to the imports) // pdfBytes, err := os.ReadFile("document.pdf") // pdfBase64 := base64.StdEncoding.EncodeToString(pdfBytes) // Send to Claude using base64 encoding client := anthropic.NewClient() message, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage( anthropic.NewDocumentBlock(anthropic.Base64PDFSourceParam{ Data: pdfBase64, }), anthropic.NewTextBlock("What are the key findings in this document?"), ), }, }) if err != nil { panic(err) } fmt.Printf("%+v\n", message.Content) ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); // Method 1: Download and encode a remote PDF String pdfUrl = "https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf"; HttpClient httpClient = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NORMAL).build(); HttpRequest request = HttpRequest.newBuilder().uri(URI.create(pdfUrl)).GET().build(); HttpResponse response = httpClient.send( request, HttpResponse.BodyHandlers.ofByteArray() ); String pdfBase64 = Base64.getEncoder().encodeToString(response.body()); // Method 2: Load from a local file // byte[] fileBytes = Files.readAllBytes(Path.of("document.pdf")); // String pdfBase64 = Base64.getEncoder().encodeToString(fileBytes); // Create document block with base64 data DocumentBlockParam documentParam = DocumentBlockParam.builder() .source(Base64PdfSource.builder().data(pdfBase64).build()) .build(); // Create a message with document and text content blocks MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .addUserMessageOfBlockParams( List.of( ContentBlockParam.ofDocument(documentParam), ContentBlockParam.ofText( TextBlockParam.builder() .text("What are the key findings in this document?") .build() ) ) ) .build(); Message message = client.messages().create(params); System.out.println(message.content()); ``` ```php PHP $client = new Client(); // First, load and encode the PDF $pdf_url = 'https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf'; $pdf_data = base64_encode(file_get_contents($pdf_url)); // Alternative: Load from a local file // $pdf_data = base64_encode(file_get_contents('document.pdf')); // Send to Claude using base64 encoding $message = $client->messages->create( maxTokens: 1024, messages: [ [ 'role' => 'user', 'content' => [ [ 'type' => 'document', 'source' => [ 'type' => 'base64', 'media_type' => 'application/pdf', 'data' => $pdf_data, ], ], [ 'type' => 'text', 'text' => 'What are the key findings in this document?', ], ], ], ], model: 'claude-opus-5', ); echo $message; ``` ```ruby Ruby require "open-uri" # First, load and encode the PDF pdf_url = "https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf" pdf_bytes = URI.open(pdf_url, "rb") { |f| f.read } pdf_data = [pdf_bytes].pack("m0") # Base64-encode without newlines # Alternative: Load from a local file # pdf_data = [File.binread("document.pdf")].pack("m0") # Send to Claude using base64 encoding anthropic = Anthropic::Client.new message = anthropic.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: [ { type: "document", source: { type: "base64", media_type: "application/pdf", data: pdf_data } }, {type: "text", text: "What are the key findings in this document?"} ] } ] ) puts(message.content) ``` #### Option 3: Files API For PDFs you'll use repeatedly, or when you want to avoid encoding overhead, use the [Files API](https://platform.claude.com/docs/en/build-with-claude/files) (beta): ```bash cURL # First, upload your PDF to the Files API FILE_ID=$(curl -sS -X POST https://api.anthropic.com/v1/files \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: files-api-2025-04-14" \ -F "file=@document.pdf" | jq -r '.id') # Then use the returned file_id in your message curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: files-api-2025-04-14" \ -d @- < { new BetaRequestDocumentBlock { Source = new BetaFileDocumentSource { FileID = fileUpload.ID }, }, new BetaTextBlockParam("What are the key findings in this document?"), }, }, ], }); Console.WriteLine(string.Join("\n", message.Content)); ``` ```go Go client := anthropic.NewClient() // Upload the PDF file pdfFile, err := os.Open("/path/to/document.pdf") if err != nil { panic(err) } defer pdfFile.Close() fileUpload, err := client.Beta.Files.Upload(context.TODO(), anthropic.BetaFileUploadParams{ File: anthropic.File(pdfFile, "document.pdf", "application/pdf"), }) if err != nil { panic(err) } // Use the uploaded file in a message message, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Betas: []anthropic.AnthropicBeta{anthropic.AnthropicBetaFilesAPI2025_04_14}, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage( anthropic.NewBetaDocumentBlock(anthropic.BetaFileDocumentSourceParam{ FileID: fileUpload.ID, }), anthropic.NewBetaTextBlock("What are the key findings in this document?"), ), }, }) if err != nil { panic(err) } fmt.Printf("%+v\n", message.Content) ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); // Upload the PDF file FileMetadata file = client .beta() .files() .upload(FileUploadParams.builder().file(Path.of("/path/to/document.pdf")).build()); // Use the uploaded file in a message MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .addBeta(AnthropicBeta.FILES_API_2025_04_14) .maxTokens(1024) .addUserMessageOfBetaContentBlockParams( List.of( BetaContentBlockParam.ofDocument( BetaRequestDocumentBlock.builder() .source( BetaFileDocumentSource.builder() .fileId(file.id()) .build() ) .build() ), BetaContentBlockParam.ofText( BetaTextBlockParam.builder() .text("What are the key findings in this document?") .build() ) ) ) .build(); BetaMessage message = client.beta().messages().create(params); System.out.println(message.content()); ``` ```php PHP use Anthropic\Core\FileParam; $client = new Client(); // Upload the PDF file $file_upload = $client->beta->files->upload( file: FileParam::fromResource(fopen('/path/to/document.pdf', 'r'), contentType: 'application/pdf'), ); // Use the uploaded file in a message $message = $client->beta->messages->create( maxTokens: 1024, betas: ['files-api-2025-04-14'], messages: [ [ 'role' => 'user', 'content' => [ [ 'type' => 'document', 'source' => [ 'type' => 'file', 'file_id' => $file_upload->id, ], ], [ 'type' => 'text', 'text' => 'What are the key findings in this document?', ], ], ], ], model: 'claude-opus-5', ); echo $message; ``` ```ruby Ruby anthropic = Anthropic::Client.new # Upload the PDF file file_upload = File.open("/path/to/document.pdf", "rb") do |f| anthropic.beta.files.upload( file: Anthropic::FilePart.new(f, filename: "document.pdf", content_type: "application/pdf") ) end # Use the uploaded file in a message message = anthropic.beta.messages.create( model: "claude-opus-5", max_tokens: 1024, betas: ["files-api-2025-04-14"], messages: [ { role: "user", content: [ { type: "document", source: {type: "file", file_id: file_upload.id} }, {type: "text", text: "What are the key findings in this document?"} ] } ] ) puts(message.content) ``` ### How PDF support works When you send a PDF to Claude, the following steps occur: * The system converts each page of the document into an image. * The text from each page is extracted and provided alongside each page's image. * Documents are provided as a combination of text and images for analysis. * This allows users to ask for insights on visual elements of a PDF, such as charts, diagrams, and other non-textual content. Claude can reference both textual and visual content when it responds. You can further improve performance by integrating PDF support with: * [Use prompt caching](https://platform.claude.com/docs/en/build-with-claude/pdf-support#use-prompt-caching): To improve performance for repeated analysis. * [Process document batches](https://platform.claude.com/docs/en/build-with-claude/pdf-support#process-document-batches): For high-volume document processing. * [Tool use](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview): To extract specific information from documents for use as tool inputs. ### Estimate your costs The token count of a PDF file depends on the total text extracted from the document and the number of pages: * Text token costs: Each page typically uses 1,500–3,000 tokens per page depending on content density. Standard API pricing applies with no additional PDF fees. * Image token costs: Because each page is converted into an image, the same [image-based cost calculations](https://platform.claude.com/docs/en/build-with-claude/vision#evaluate-image-size) are applied. You can use [token counting](https://platform.claude.com/docs/en/build-with-claude/token-counting) to estimate costs for your specific PDFs. ## Optimize PDF processing ### Improve performance Follow these best practices for optimal results: * Place PDFs before text in your requests * Use standard fonts * Ensure text is clear and legible * Rotate pages to proper upright orientation * Use logical page numbers (from PDF viewer) in prompts * Split large PDFs into chunks when needed * Enable prompt caching for repeated analysis ### Scale your implementation For high-volume processing, consider these approaches: #### Use prompt caching Cache PDFs with [prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) to improve performance on repeated queries: ```bash cURL curl -sL "https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf" | base64 | tr -d '\n' > pdf_base64.txt # Create a JSON request file using the pdf_base64.txt content jq -n --rawfile PDF_BASE64 pdf_base64.txt '{ "model": "claude-opus-5", "max_tokens": 1024, "messages": [{ "role": "user", "content": [{ "type": "document", "source": { "type": "base64", "media_type": "application/pdf", "data": $PDF_BASE64 }, "cache_control": { "type": "ephemeral" } }, { "type": "text", "text": "Which model has the highest human preference win rates across each use-case?" }] }] }' > request.json # Then make the API call using the JSON file curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d @request.json ``` ```bash CLI ant messages create --transform content --format yaml <<'YAML' model: claude-opus-5 max_tokens: 1024 messages: - role: user content: - type: document source: type: base64 media_type: application/pdf data: "@./document.pdf" cache_control: type: ephemeral - type: text text: Which model has the highest human preference win rates across each use-case? YAML ``` ```python Python import base64 import httpx # First, load and encode the PDF pdf_url = "https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf" pdf_data = base64.standard_b64encode( httpx.get(pdf_url, follow_redirects=True).content ).decode("utf-8") # Create a message with the cached document client = anthropic.Anthropic() message = client.messages.create( model="claude-opus-5", max_tokens=1024, messages=[ { "role": "user", "content": [ { "type": "document", "source": { "type": "base64", "media_type": "application/pdf", "data": pdf_data, }, "cache_control": {"type": "ephemeral"}, }, { "type": "text", "text": "Which model has the highest human preference win rates across each use-case?", }, ], } ], ) print(message.content) ``` ```typescript TypeScript // First, load and encode the PDF const pdfURL = "https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf"; const pdfResponse = await fetch(pdfURL); const arrayBuffer = await pdfResponse.arrayBuffer(); const pdfBase64 = Buffer.from(arrayBuffer).toString("base64"); // Create a message with the cached document const anthropic = new Anthropic(); const response = await anthropic.messages.create({ model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: [ { type: "document", source: { type: "base64", media_type: "application/pdf", data: pdfBase64 }, cache_control: { type: "ephemeral" } }, { type: "text", text: "Which model has the highest human preference win rates across each use-case?" } ] } ] }); console.log(response); ``` ```csharp C# var client = new AnthropicClient(); // Download and encode the PDF var pdfUrl = "https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf"; using var httpClient = new HttpClient(); var pdfBase64 = Convert.ToBase64String(await httpClient.GetByteArrayAsync(pdfUrl)); var message = await client.Messages.Create(new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [ new() { Role = Role.User, Content = new List { new DocumentBlockParam { Source = new Base64PdfSource { Data = pdfBase64 }, CacheControl = new CacheControlEphemeral(), }, new TextBlockParam("Which model has the highest human preference win rates across each use-case?"), }, }, ], }); Console.WriteLine(message); ``` ```go Go // First, load and encode the PDF pdfURL := "https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf" resp, err := http.Get(pdfURL) if err != nil { panic(err) } defer resp.Body.Close() pdfBytes, err := io.ReadAll(resp.Body) if err != nil { panic(err) } pdfBase64 := base64.StdEncoding.EncodeToString(pdfBytes) // Create a document block with cache control client := anthropic.NewClient() message, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage( anthropic.ContentBlockParamUnion{ OfDocument: &anthropic.DocumentBlockParam{ Source: anthropic.DocumentBlockParamSourceUnion{ OfBase64: &anthropic.Base64PDFSourceParam{ Data: pdfBase64, }, }, CacheControl: anthropic.NewCacheControlEphemeralParam(), }, }, anthropic.NewTextBlock("Which model has the highest human preference win rates across each use-case?"), ), }, }) if err != nil { panic(err) } fmt.Printf("%+v\n", message.Content) ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); // Download and encode the PDF String pdfUrl = "https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf"; HttpClient httpClient = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NORMAL).build(); HttpRequest request = HttpRequest.newBuilder().uri(URI.create(pdfUrl)).GET().build(); HttpResponse response = httpClient.send( request, HttpResponse.BodyHandlers.ofByteArray() ); String pdfBase64 = Base64.getEncoder().encodeToString(response.body()); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .addUserMessageOfBlockParams( List.of( ContentBlockParam.ofDocument( DocumentBlockParam.builder() .source(Base64PdfSource.builder().data(pdfBase64).build()) .cacheControl(CacheControlEphemeral.builder().build()) .build() ), ContentBlockParam.ofText( TextBlockParam.builder() .text( "Which model has the highest human preference win rates across each use-case?" ) .build() ) ) ) .build(); Message message = client.messages().create(params); System.out.println(message); ``` ```php PHP $client = new Client(); // Load and encode the PDF $pdf_url = 'https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf'; $pdf_data = base64_encode(file_get_contents($pdf_url)); $message = $client->messages->create( maxTokens: 1024, messages: [ [ 'role' => 'user', 'content' => [ [ 'type' => 'document', 'source' => [ 'type' => 'base64', 'media_type' => 'application/pdf', 'data' => $pdf_data, ], 'cache_control' => ['type' => 'ephemeral'], ], [ 'type' => 'text', 'text' => 'Which model has the highest human preference win rates across each use-case?', ], ], ], ], model: 'claude-opus-5', ); echo $message; ``` ```ruby Ruby require "open-uri" # Load and encode the PDF pdf_url = "https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf" pdf_bytes = URI.open(pdf_url, "rb") { |f| f.read } pdf_data = [pdf_bytes].pack("m0") # Base64-encode without newlines anthropic = Anthropic::Client.new message = anthropic.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: [ { type: "document", source: { type: "base64", media_type: "application/pdf", data: pdf_data }, cache_control: {type: "ephemeral"} }, { type: "text", text: "Which model has the highest human preference win rates across each use-case?" } ] } ] ) puts(message.content) ``` #### Process document batches Use the [Message Batches API](https://platform.claude.com/docs/en/build-with-claude/batch-processing) to process many PDFs in one request: ```bash cURL curl -sL "https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf" | base64 | tr -d '\n' > pdf_base64.txt # Create a JSON request file using the pdf_base64.txt content jq -n --rawfile PDF_BASE64 pdf_base64.txt '{ "requests": [ { "custom_id": "my-first-request", "params": { "model": "claude-opus-5", "max_tokens": 1024, "messages": [{ "role": "user", "content": [{ "type": "document", "source": { "type": "base64", "media_type": "application/pdf", "data": $PDF_BASE64 } }, { "type": "text", "text": "Which model has the highest human preference win rates across each use-case?" }] }] } }, { "custom_id": "my-second-request", "params": { "model": "claude-opus-5", "max_tokens": 1024, "messages": [{ "role": "user", "content": [{ "type": "document", "source": { "type": "base64", "media_type": "application/pdf", "data": $PDF_BASE64 } }, { "type": "text", "text": "Extract 5 key insights from this document." }] }] } }] }' > request.json # Then make the API call using the JSON file curl https://api.anthropic.com/v1/messages/batches \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d @request.json ``` ```bash CLI ant messages:batches create <<'YAML' requests: - custom_id: my-first-request params: model: claude-opus-5 max_tokens: 1024 messages: - role: user content: - type: document source: type: base64 media_type: application/pdf data: "@./document.pdf" - type: text text: >- Which model has the highest human preference win rates across each use-case? - custom_id: my-second-request params: model: claude-opus-5 max_tokens: 1024 messages: - role: user content: - type: document source: type: base64 media_type: application/pdf data: "@./document.pdf" - type: text text: Extract 5 key insights from this document. YAML ``` ```python Python import base64 import httpx # First, load and encode the PDF pdf_url = "https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf" pdf_data = base64.standard_b64encode( httpx.get(pdf_url, follow_redirects=True).content ).decode("utf-8") # Create a batch of requests that use the document client = anthropic.Anthropic() message_batch = client.messages.batches.create( requests=[ { "custom_id": "my-first-request", "params": { "model": "claude-opus-5", "max_tokens": 1024, "messages": [ { "role": "user", "content": [ { "type": "document", "source": { "type": "base64", "media_type": "application/pdf", "data": pdf_data, }, }, { "type": "text", "text": "Which model has the highest human preference win rates across each use-case?", }, ], } ], }, }, { "custom_id": "my-second-request", "params": { "model": "claude-opus-5", "max_tokens": 1024, "messages": [ { "role": "user", "content": [ { "type": "document", "source": { "type": "base64", "media_type": "application/pdf", "data": pdf_data, }, }, { "type": "text", "text": "Extract 5 key insights from this document.", }, ], } ], }, }, ] ) print(message_batch) ``` ```typescript TypeScript // First, load and encode the PDF const pdfURL = "https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf"; const pdfResponse = await fetch(pdfURL); const arrayBuffer = await pdfResponse.arrayBuffer(); const pdfBase64 = Buffer.from(arrayBuffer).toString("base64"); // Create a batch of requests that use the document const anthropic = new Anthropic(); const response = await anthropic.messages.batches.create({ requests: [ { custom_id: "my-first-request", params: { model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: [ { type: "document", source: { type: "base64", media_type: "application/pdf", data: pdfBase64 } }, { type: "text", text: "Which model has the highest human preference win rates across each use-case?" } ] } ] } }, { custom_id: "my-second-request", params: { model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: [ { type: "document", source: { type: "base64", media_type: "application/pdf", data: pdfBase64 } }, { type: "text", text: "Extract 5 key insights from this document." } ] } ] } } ] }); console.log(response); ``` ```csharp C# var client = new AnthropicClient(); // Download and encode the PDF var pdfUrl = "https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf"; using var httpClient = new HttpClient(); var pdfBase64 = Convert.ToBase64String(await httpClient.GetByteArrayAsync(pdfUrl)); var batch = await client.Messages.Batches.Create(new BatchCreateParams { Requests = [ new() { CustomID = "my-first-request", Params = new() { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [ new() { Role = Role.User, Content = new List { new DocumentBlockParam { Source = new Base64PdfSource { Data = pdfBase64 }, }, new TextBlockParam("Which model has the highest human preference win rates across each use-case?"), }, }, ], }, }, new() { CustomID = "my-second-request", Params = new() { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [ new() { Role = Role.User, Content = new List { new DocumentBlockParam { Source = new Base64PdfSource { Data = pdfBase64 }, }, new TextBlockParam("Extract 5 key insights from this document."), }, }, ], }, }, ], }); Console.WriteLine(batch); ``` ```go Go // First, load and encode the PDF pdfURL := "https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf" resp, err := http.Get(pdfURL) if err != nil { panic(err) } defer resp.Body.Close() pdfBytes, err := io.ReadAll(resp.Body) if err != nil { panic(err) } pdfBase64 := base64.StdEncoding.EncodeToString(pdfBytes) // Create a batch of requests that use the document client := anthropic.NewClient() batch, err := client.Messages.Batches.New(context.TODO(), anthropic.MessageBatchNewParams{ Requests: []anthropic.MessageBatchNewParamsRequest{ { CustomID: "my-first-request", Params: anthropic.MessageBatchNewParamsRequestParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage( anthropic.NewDocumentBlock(anthropic.Base64PDFSourceParam{ Data: pdfBase64, }), anthropic.NewTextBlock("Which model has the highest human preference win rates across each use-case?"), ), }, }, }, { CustomID: "my-second-request", Params: anthropic.MessageBatchNewParamsRequestParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage( anthropic.NewDocumentBlock(anthropic.Base64PDFSourceParam{ Data: pdfBase64, }), anthropic.NewTextBlock("Extract 5 key insights from this document."), ), }, }, }, }, }) if err != nil { panic(err) } fmt.Printf("%+v\n", batch) ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); // Download and encode the PDF String pdfUrl = "https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf"; HttpClient httpClient = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NORMAL).build(); HttpRequest request = HttpRequest.newBuilder().uri(URI.create(pdfUrl)).GET().build(); HttpResponse response = httpClient.send( request, HttpResponse.BodyHandlers.ofByteArray() ); String pdfBase64 = Base64.getEncoder().encodeToString(response.body()); BatchCreateParams params = BatchCreateParams.builder() .addRequest( BatchCreateParams.Request.builder() .customId("my-first-request") .params( BatchCreateParams.Request.Params.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .addUserMessageOfBlockParams( List.of( ContentBlockParam.ofDocument( DocumentBlockParam.builder() .source(Base64PdfSource.builder().data(pdfBase64).build()) .build() ), ContentBlockParam.ofText( TextBlockParam.builder() .text( "Which model has the highest human preference win rates across each use-case?" ) .build() ) ) ) .build() ) .build() ) .addRequest( BatchCreateParams.Request.builder() .customId("my-second-request") .params( BatchCreateParams.Request.Params.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .addUserMessageOfBlockParams( List.of( ContentBlockParam.ofDocument( DocumentBlockParam.builder() .source(Base64PdfSource.builder().data(pdfBase64).build()) .build() ), ContentBlockParam.ofText( TextBlockParam.builder() .text("Extract 5 key insights from this document.") .build() ) ) ) .build() ) .build() ) .build(); MessageBatch batch = client.messages().batches().create(params); System.out.println(batch); ``` ```php PHP $client = new Client(); // Load and encode the PDF $pdf_url = 'https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf'; $pdf_data = base64_encode(file_get_contents($pdf_url)); $batch = $client->messages->batches->create( requests: [ [ 'custom_id' => 'my-first-request', 'params' => [ 'model' => 'claude-opus-5', 'max_tokens' => 1024, 'messages' => [ [ 'role' => 'user', 'content' => [ [ 'type' => 'document', 'source' => [ 'type' => 'base64', 'media_type' => 'application/pdf', 'data' => $pdf_data, ], ], [ 'type' => 'text', 'text' => 'Which model has the highest human preference win rates across each use-case?', ], ], ], ], ], ], [ 'custom_id' => 'my-second-request', 'params' => [ 'model' => 'claude-opus-5', 'max_tokens' => 1024, 'messages' => [ [ 'role' => 'user', 'content' => [ [ 'type' => 'document', 'source' => [ 'type' => 'base64', 'media_type' => 'application/pdf', 'data' => $pdf_data, ], ], [ 'type' => 'text', 'text' => 'Extract 5 key insights from this document.', ], ], ], ], ], ], ], ); echo $batch; ``` ```ruby Ruby require "open-uri" # Load and encode the PDF pdf_url = "https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf" pdf_bytes = URI.open(pdf_url, "rb") { |f| f.read } pdf_data = [pdf_bytes].pack("m0") # Base64-encode without newlines anthropic = Anthropic::Client.new message_batch = anthropic.messages.batches.create( requests: [ { custom_id: "my-first-request", params: { model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: [ { type: "document", source: { type: "base64", media_type: "application/pdf", data: pdf_data } }, { type: "text", text: "Which model has the highest human preference win rates across each use-case?" } ] } ] } }, { custom_id: "my-second-request", params: { model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: [ { type: "document", source: { type: "base64", media_type: "application/pdf", data: pdf_data } }, { type: "text", text: "Extract 5 key insights from this document." } ] } ] } } ] ) puts(message_batch) ``` Batches process asynchronously. To check progress and retrieve results once processing ends, see [Batch processing](https://platform.claude.com/docs/en/build-with-claude/batch-processing). ## Next steps Claude's vision capabilities allow it to understand and analyze images, opening up exciting possibilities for multimodal interaction. Explore practical examples of PDF processing in the Claude Cookbook recipe. See complete API documentation for PDF support. ### Working with files > Images and vision --- title: Coordinates and bounding boxes url: https://platform.claude.com/docs/en/build-with-claude/vision-coordinates description: How Claude resizes images, and how to work with the pixel coordinates it returns for bounding boxes, points, and UI elements. --- Claude can locate and label regions of an image (for example, returning bounding boxes for tables, form fields, chart elements, or UI components). This guide covers how Claude resizes images before processing them and how to work with the pixel coordinates it returns, so that boxes and points line up with your original image. You'll need this for OCR pipelines, form extraction, chart parsing, UI element location, and any task where you act on a specific region of an image. For sending images, supported formats, and per-model resolution limits, see [Vision](https://platform.claude.com/docs/en/build-with-claude/vision). **Claude works best with absolute pixel coordinates.** Ask for them explicitly in your prompt. For example: *"Return the bounding box of each table as `[x1, y1, x2, y2]` (top-left and bottom-right corners) in pixel coordinates."* Claude does not work well when you ask for normalized coordinates, for example: *"Return bounding box coordinates between `0` and `1000`."* Always ask for pixel coordinates and normalize in your own code if you need to. To get coordinates as machine-readable JSON instead of prose, define a schema with [structured outputs](https://platform.claude.com/docs/en/build-with-claude/structured-outputs), for example an object with an `[x1, y1, x2, y2]` array per detected element. Coordinates follow the standard image convention: the origin `(0, 0)` is the top-left corner of the image, with x increasing to the right and y increasing downward. The coordinates Claude returns are pixel positions in the image Claude sees: your image after Claude resizes it to fit the model's native resolution (see [How Claude resizes and pads images](https://platform.claude.com/docs/en/build-with-claude/vision-coordinates#how-claude-resizes-and-pads-images)). To get coordinates you can use directly, either pre-resize your image so the coordinates map one-to-one onto the image you have (see [Resize your image before uploading](https://platform.claude.com/docs/en/build-with-claude/vision-coordinates#resize-your-image-before-uploading)), or rescale the coordinates Claude returns (see [Rescale coordinates when you cannot pre-resize](https://platform.claude.com/docs/en/build-with-claude/vision-coordinates#rescale-coordinates-when-you-cannot-pre-resize)). Claude's spatial reasoning has limits (see [Limitations](https://platform.claude.com/docs/en/build-with-claude/vision#limitations)). Coordinate accuracy is best when you state the expected coordinate format in your prompt and spot-check results visually before processing at scale. Small elements lose precision when an image is downscaled: for fine targets, crop the region of interest and send the crop (offset returned coordinates by the crop origin), or use a high-resolution-tier model. For [PDF support](https://platform.claude.com/docs/en/build-with-claude/pdf-support), pages are rasterized to images server-side at dimensions you don't control, so the returned coordinates can't be reliably mapped back onto the page. To work with coordinates on PDF content, rasterize the pages to images yourself and use the pre-resize approach. ## How Claude resizes and pads images Claude finds the largest aspect-preserving size that satisfies both of the model's image limits: 1. **Edge limit:** neither side exceeds the maximum edge length (1568 px on the standard tier, 2576 px on the high-resolution tier). 2. **Visual token limit:** the image's token cost `⌈width / 28⌉ × ⌈height / 28⌉` does not exceed the model's visual token budget (1568 tokens on the standard tier, 4784 on the high-resolution tier). See [Resolution and token cost](https://platform.claude.com/docs/en/build-with-claude/vision#evaluate-image-size) for which models are in which tier. For nearly all photos and screenshots, the visual token limit is what determines the final size. The edge limit takes over only for elongated images such as panoramas or tall phone screenshots. Compute the size with the [reference implementation](https://platform.claude.com/docs/en/build-with-claude/vision-coordinates#resize-your-image-before-uploading) rather than scaling to the edge length by hand: a 1920×1080 screenshot resizes to 1456×819, not 1568×882, and assuming the edge limit puts every coordinate noticeably off target. The token limit can also trigger a resize when neither side exceeds the edge limit. Overlooking this is the most common cause of misaligned coordinates. For example, an A4 page scanned at 130 DPI is 1075×1520 pixels: both sides are under 1568 px, but it costs `39 × 55 = 2145` visual tokens, so Claude resizes it to 924×1307. This example assumes a model on the standard resolution tier. A high-resolution-tier model doesn't resize the same scan: 2145 tokens is within its 4784-token budget, so the coordinates it returns map directly onto the 1075×1520 original. Model tiers are listed in [Resolution and token cost](https://platform.claude.com/docs/en/build-with-claude/vision#evaluate-image-size). Claude then pads every image, resized or not, up to the next multiple of 28 pixels on the bottom and right edges (924×1307 becomes 924×1316 in the example). The padding contains no content: Claude perceives the padded image, but the page content only ever occupies the un-padded resized region. **Always normalize or rescale by the resized dimensions, not the padded dimensions**; dividing by the padded dimensions scales every coordinate by a small amount. ## Resize your image before uploading The most reliable approach is to resize your image yourself before uploading, so the image you have is exactly the image Claude sees and the coordinates Claude returns need no conversion. First check which resolution tier your model is on (see [Resolution and token cost](https://platform.claude.com/docs/en/build-with-claude/vision#evaluate-image-size)) and pass the matching edge and token limits. The following reference implementation computes the exact size Claude resizes an image to: ```bash cURL # This reference implementation is local math that makes no API request, so # there's nothing to show for cURL. See the SDK tabs. ``` ```bash CLI # This reference implementation is local math that makes no API request, so # there's nothing to show for the CLI. See the SDK tabs. ``` ```python Python import math def count_image_tokens(width: int, height: int) -> int: """Visual tokens consumed by an image: one token per 28x28 pixel patch.""" return math.ceil(width / 28) * math.ceil(height / 28) def resized_size( width: int, height: int, max_edge: int = 1568, max_tokens: int = 1568, ) -> tuple[int, int]: """The size Claude resizes an image to before padding. Defaults are for the standard resolution tier. For high-resolution-tier models, use max_edge=2576 and max_tokens=4784. Returns (width, height). Images that already fit within the limits are returned unchanged. """ def fits(w: int, h: int) -> bool: return ( math.ceil(w / 28) * 28 <= max_edge and math.ceil(h / 28) * 28 <= max_edge and count_image_tokens(w, h) <= max_tokens ) if fits(width, height): return (width, height) if height > width: resized_h, resized_w = resized_size(height, width, max_edge, max_tokens) return (resized_w, resized_h) # Binary search along the long edge for the largest aspect-preserving # size that fits. aspect_ratio = width / height lo, hi = 1, width # lo always fits; hi never fits while lo + 1 < hi: mid = (lo + hi) // 2 if fits(mid, max(round(mid / aspect_ratio), 1)): lo = mid else: hi = mid return (lo, max(round(lo / aspect_ratio), 1)) # The A4 example from "How Claude resizes and pads images": print(resized_size(1075, 1520)) # (924, 1307) # To apply the resize, use your image library, for example Pillow: # image.resize(resized_size(*image.size)) ``` ```typescript TypeScript /** Visual tokens consumed by an image: one token per 28x28 pixel patch. */ function countImageTokens(width: number, height: number): number { return Math.ceil(width / 28) * Math.ceil(height / 28); } /** * Round half to even (banker's rounding), matching Python's round(). The * live API resolves exact .5 ties toward the even neighbor, so Math.round * (which rounds halves up) would compute a different size for some images. */ function roundTiesToEven(value: number): number { const floor = Math.floor(value); if (value - floor !== 0.5) return Math.round(value); return floor % 2 === 0 ? floor : floor + 1; } /** * The size Claude resizes an image to before padding. * * Defaults are for the standard resolution tier. For high-resolution-tier * models, use maxEdge = 2576 and maxTokens = 4784. Returns [width, height]. * Images that already fit within the limits are returned unchanged. */ function resizedSize( width: number, height: number, maxEdge = 1568, maxTokens = 1568 ): [number, number] { const fits = (w: number, h: number): boolean => Math.ceil(w / 28) * 28 <= maxEdge && Math.ceil(h / 28) * 28 <= maxEdge && countImageTokens(w, h) <= maxTokens; if (fits(width, height)) return [width, height]; if (height > width) { const [resizedH, resizedW] = resizedSize(height, width, maxEdge, maxTokens); return [resizedW, resizedH]; } // Binary search along the long edge for the largest aspect-preserving // size that fits. const aspectRatio = width / height; let lo = 1; // lo always fits let hi = width; // hi never fits while (lo + 1 < hi) { const mid = Math.floor((lo + hi) / 2); if (fits(mid, Math.max(roundTiesToEven(mid / aspectRatio), 1))) { lo = mid; } else { hi = mid; } } return [lo, Math.max(roundTiesToEven(lo / aspectRatio), 1)]; } // The A4 example from "How Claude resizes and pads images": console.log(resizedSize(1075, 1520)); // [ 924, 1307 ] // To apply the resize, use your image library, for example sharp: // await sharp(input).resize(width, height).toBuffer() ``` ```csharp C# // Visual tokens consumed by an image: one token per 28x28 pixel patch. static int CountImageTokens(int width, int height) { return (width + 27) / 28 * ((height + 27) / 28); // ceil(w/28) * ceil(h/28) } // The size Claude resizes an image to before padding. Defaults are for the // standard resolution tier; for high-resolution-tier models, pass // maxEdge: 2576, maxTokens: 4784. Images that already fit within the limits // are returned unchanged. static (int Width, int Height) ResizedSize( int width, int height, int maxEdge = 1568, int maxTokens = 1568) { bool Fits(int w, int h) => (w + 27) / 28 * 28 <= maxEdge && (h + 27) / 28 * 28 <= maxEdge && CountImageTokens(w, h) <= maxTokens; if (Fits(width, height)) { return (width, height); } if (height > width) { (int resizedH, int resizedW) = ResizedSize(height, width, maxEdge, maxTokens); return (resizedW, resizedH); } // Binary search along the long edge for the largest aspect-preserving // size that fits. The short edge rounds half to even, matching the live // API at exact .5 ties (MidpointRounding.ToEven, Math.Round's default). double aspectRatio = (double)width / height; int lo = 1; // lo always fits int hi = width; // hi never fits while (lo + 1 < hi) { int mid = (lo + hi) / 2; if (Fits(mid, ShortEdge(mid))) { lo = mid; } else { hi = mid; } } return (lo, ShortEdge(lo)); int ShortEdge(int longEdge) => Math.Max((int)Math.Round(longEdge / aspectRatio, MidpointRounding.ToEven), 1); } // The A4 example from "How Claude resizes and pads images": Console.WriteLine(ResizedSize(1075, 1520)); // (924, 1307) ``` ```go Go // countImageTokens is the visual tokens consumed by an image: one token per // 28x28 pixel patch. func countImageTokens(width, height int) int { return ((width + 27) / 28) * ((height + 27) / 28) // ceil(w/28) * ceil(h/28) } // resizedSize is the size Claude resizes an image to before padding, as // (width, height). Pass maxEdge 1568 and maxTokens 1568 for the standard // resolution tier, or 2576 and 4784 for the high-resolution tier. Images // that already fit within the limits are returned unchanged. // The A4 example from "How Claude resizes and pads images": // resizedSize(1075, 1520, 1568, 1568) returns (924, 1307). func resizedSize(width, height, maxEdge, maxTokens int) (int, int) { fits := func(w, h int) bool { return ((w+27)/28)*28 <= maxEdge && ((h+27)/28)*28 <= maxEdge && countImageTokens(w, h) <= maxTokens } if fits(width, height) { return width, height } if height > width { resizedH, resizedW := resizedSize(height, width, maxEdge, maxTokens) return resizedW, resizedH } // Binary search along the long edge for the largest aspect-preserving // size that fits. The short edge rounds half to even (math.RoundToEven), // matching the live API at exact .5 ties; math.Round would round them up. aspectRatio := float64(width) / float64(height) lo, hi := 1, width // lo always fits; hi never fits for lo+1 < hi { mid := (lo + hi) / 2 short := max(int(math.RoundToEven(float64(mid)/aspectRatio)), 1) if fits(mid, short) { lo = mid } else { hi = mid } } return lo, max(int(math.RoundToEven(float64(lo)/aspectRatio)), 1) } ``` ```java Java /** A resized image size, as returned by resizedSize. */ record Size(int width, int height) {} /** Visual tokens consumed by an image: one token per 28x28 pixel patch. */ static int countImageTokens(int width, int height) { return Math.ceilDiv(width, 28) * Math.ceilDiv(height, 28); } /** * The size Claude resizes an image to before padding. * *

Pass maxEdge 1568 and maxTokens 1568 for the standard resolution tier, * or 2576 and 4784 for the high-resolution tier. Images that already fit * within the limits are returned unchanged. * *

The A4 example from "How Claude resizes and pads images": * resizedSize(1075, 1520, 1568, 1568) returns new Size(924, 1307). */ static Size resizedSize(int width, int height, int maxEdge, int maxTokens) { if (fits(width, height, maxEdge, maxTokens)) { return new Size(width, height); } if (height > width) { Size rotated = resizedSize(height, width, maxEdge, maxTokens); return new Size(rotated.height(), rotated.width()); } // Binary search along the long edge for the largest aspect-preserving // size that fits. The short edge rounds half to even (Math.rint), // matching the live API at exact .5 ties; Math.round would round them up. double aspectRatio = (double) width / height; int lo = 1; // lo always fits int hi = width; // hi never fits while (lo + 1 < hi) { int mid = (lo + hi) / 2; if (fits(mid, shortEdge(mid, aspectRatio), maxEdge, maxTokens)) { lo = mid; } else { hi = mid; } } return new Size(lo, shortEdge(lo, aspectRatio)); } private static boolean fits(int width, int height, int maxEdge, int maxTokens) { return Math.ceilDiv(width, 28) * 28 <= maxEdge && Math.ceilDiv(height, 28) * 28 <= maxEdge && countImageTokens(width, height) <= maxTokens; } private static int shortEdge(int longEdge, double aspectRatio) { return Math.max((int) Math.rint(longEdge / aspectRatio), 1); } ``` ```php PHP // Visual tokens consumed by an image: one token per 28x28 pixel patch. function countImageTokens(int $width, int $height): int { return intdiv($width + 27, 28) * intdiv($height + 27, 28); } /** * The size Claude resizes an image to before padding, as [width, height]. * * Defaults are for the standard resolution tier. For high-resolution-tier * models, pass maxEdge: 2576, maxTokens: 4784. Images that already fit * within the limits are returned unchanged. */ function resizedSize(int $width, int $height, int $maxEdge = 1568, int $maxTokens = 1568): array { $fits = fn (int $w, int $h): bool => intdiv($w + 27, 28) * 28 <= $maxEdge && intdiv($h + 27, 28) * 28 <= $maxEdge && countImageTokens($w, $h) <= $maxTokens; if ($fits($width, $height)) { return [$width, $height]; } if ($height > $width) { [$resizedH, $resizedW] = resizedSize($height, $width, $maxEdge, $maxTokens); return [$resizedW, $resizedH]; } // Binary search along the long edge for the largest aspect-preserving // size that fits. The short edge rounds half to even // (PHP_ROUND_HALF_EVEN), matching the live API at exact .5 ties. $aspectRatio = $width / $height; $lo = 1; // lo always fits $hi = $width; // hi never fits while ($lo + 1 < $hi) { $mid = intdiv($lo + $hi, 2); $short = max((int) round($mid / $aspectRatio, 0, PHP_ROUND_HALF_EVEN), 1); if ($fits($mid, $short)) { $lo = $mid; } else { $hi = $mid; } } return [$lo, max((int) round($lo / $aspectRatio, 0, PHP_ROUND_HALF_EVEN), 1)]; } // The A4 example from "How Claude resizes and pads images": [$resizedWidth, $resizedHeight] = resizedSize(1075, 1520); echo "({$resizedWidth}, {$resizedHeight})\n"; // (924, 1307) ``` ```ruby Ruby # Visual tokens consumed by an image: one token per 28x28 pixel patch. def count_image_tokens(width, height) width.ceildiv(28) * height.ceildiv(28) end # The size Claude resizes an image to before padding, as [width, height]. # # Defaults are for the standard resolution tier. For high-resolution-tier # models, pass max_edge: 2576, max_tokens: 4784. Images that already fit # within the limits are returned unchanged. def resized_size(width, height, max_edge = 1568, max_tokens = 1568) fits = lambda do |w, h| w.ceildiv(28) * 28 <= max_edge && h.ceildiv(28) * 28 <= max_edge && count_image_tokens(w, h) <= max_tokens end return [width, height] if fits.call(width, height) if height > width resized_h, resized_w = resized_size(height, width, max_edge, max_tokens) return [resized_w, resized_h] end # Binary search along the long edge for the largest aspect-preserving # size that fits. The short edge rounds half to even (round(half: :even)), # matching the live API at exact .5 ties. aspect_ratio = width.fdiv(height) lo = 1 # lo always fits hi = width # hi never fits while lo + 1 < hi mid = (lo + hi) / 2 short = [(mid / aspect_ratio).round(half: :even), 1].max if fits.call(mid, short) lo = mid else hi = mid end end [lo, [(lo / aspect_ratio).round(half: :even), 1].max] end # The A4 example from "How Claude resizes and pads images": p resized_size(1075, 1520) # => [924, 1307] ``` 1. Resize the image to the dimensions returned by the resize helper. If the image already fits within the model's limits, the helper returns its dimensions unchanged and no resize is needed. 2. [Send the resized image](https://platform.claude.com/docs/en/build-with-claude/vision#send-images-to-claude) to the API. Don't pad it yourself. Claude handles padding, and padding doesn't shift the coordinate origin. 3. In your prompt, ask explicitly for pixel coordinates. For example: *"Return the click point for the Submit button as `[x, y]` in pixel coordinates."* 4. Use the returned coordinates directly against the image you sent. If you need normalized coordinates, divide by the dimensions of the image you sent, not by the original image's dimensions and not by the padded dimensions. The [Token counting](https://platform.claude.com/docs/en/build-with-claude/token-counting) endpoint estimates an image's token cost from its dimensions without fully processing it, so a successful count doesn't mean the image is within the Messages API's [request limits](https://platform.claude.com/docs/en/build-with-claude/vision#request-limits). An image can count successfully and still be rejected when you send it. ## Rescale coordinates when you cannot pre-resize If you cannot pre-resize (for example, when the image comes from an upstream system you can't modify), use the resize helper from [Resize your image before uploading](https://platform.claude.com/docs/en/build-with-claude/vision-coordinates#resize-your-image-before-uploading) to recover the dimensions Claude saw, then map the coordinates Claude returns into normalized coordinates or back onto your original image. Claude resizes oversized images rather than rejecting them, up to the API's [request limits](https://platform.claude.com/docs/en/build-with-claude/vision#request-limits). Beyond those limits the request fails with a validation error instead. Pass the tier limits that match the model you called: the wrong tier's limits recover the wrong resized dimensions and silently shift every coordinate. This approach requires knowing the pixel dimensions of the image you uploaded, so it does not apply to PDF uploads. ```bash cURL # This local coordinate conversion makes no API request, so there's nothing # to show for cURL. See the SDK tabs. ``` ```bash CLI # This local coordinate conversion makes no API request, so there's nothing # to show for the CLI. See the SDK tabs. ``` ```python Python # This helper calls resized_size from the resize example on this page. def to_relative_coordinates( x: float, y: float, original_width: int, original_height: int, max_edge: int = 1568, max_tokens: int = 1568, ) -> tuple[float, float]: """Map a pixel coordinate returned by Claude to relative coordinates in [0, 1]. Pass the dimensions of the image you uploaded. For high-resolution-tier models, use max_edge=2576 and max_tokens=4784. """ resized_w, resized_h = resized_size( original_width, original_height, max_edge, max_tokens ) return (x / resized_w, y / resized_h) # A table corner Claude returns at (462, 653.5) on the resized A4 page maps # back onto the 1075x1520 original like this: rel_x, rel_y = to_relative_coordinates(462, 653.5, 1075, 1520) print((rel_x * 1075, rel_y * 1520)) # (537.5, 760.0) ``` ```typescript TypeScript // This helper calls resizedSize from the resize example on this page. /** * Map a pixel coordinate returned by Claude to relative coordinates in [0, 1]. * * Pass the dimensions of the image you uploaded. For high-resolution-tier * models, use maxEdge = 2576 and maxTokens = 4784. */ function toRelativeCoordinates( x: number, y: number, originalWidth: number, originalHeight: number, maxEdge = 1568, maxTokens = 1568 ): [number, number] { const [resizedW, resizedH] = resizedSize( originalWidth, originalHeight, maxEdge, maxTokens ); return [x / resizedW, y / resizedH]; } // A table corner Claude returns at (462, 653.5) on the resized A4 page maps // back onto the 1075x1520 original like this: const [relX, relY] = toRelativeCoordinates(462, 653.5, 1075, 1520); console.log([relX * 1075, relY * 1520]); // [ 537.5, 760 ] ``` ```csharp C# // This helper calls ResizedSize from the resize example on this page. // Map a pixel coordinate returned by Claude to relative coordinates in // [0, 1]. Pass the dimensions of the image you uploaded, and the same tier // limits used for ResizedSize. static (double X, double Y) ToRelativeCoordinates( double x, double y, int originalWidth, int originalHeight, int maxEdge = 1568, int maxTokens = 1568) { (int resizedW, int resizedH) = ResizedSize(originalWidth, originalHeight, maxEdge, maxTokens); return (x / resizedW, y / resizedH); } // A table corner Claude returns at (462, 653.5) on the resized A4 page maps // back onto the 1075x1520 original like this: (double relX, double relY) = ToRelativeCoordinates(462, 653.5, 1075, 1520); Console.WriteLine((relX * 1075, relY * 1520)); // (537.5, 760) ``` ```go Go // This helper calls resizedSize from the resize example on this page. // toRelativeCoordinates maps a pixel coordinate returned by Claude to // relative coordinates in [0, 1]. Pass the dimensions of the image you // uploaded, and the same tier limits used for resizedSize. func toRelativeCoordinates( x, y float64, originalWidth, originalHeight, maxEdge, maxTokens int, ) (float64, float64) { resizedW, resizedH := resizedSize(originalWidth, originalHeight, maxEdge, maxTokens) return x / float64(resizedW), y / float64(resizedH) } // To map back to your original image's pixel space, multiply by the original // dimensions: a table corner returned at (462, 653.5) on the resized A4 page // is (relX*1075, relY*1520) = (537.5, 760) on the 1075x1520 original. ``` ```java Java // This helper calls resizedSize from the resize example on this page. /** A coordinate scaled into the [0, 1] range on both axes. */ record RelativeCoordinate(double x, double y) {} /** * Map a pixel coordinate returned by Claude to relative coordinates in * [0, 1]. Pass the dimensions of the image you uploaded, and the same tier * limits used for resizedSize. */ static RelativeCoordinate toRelativeCoordinates( double x, double y, int originalWidth, int originalHeight, int maxEdge, int maxTokens) { Size resized = resizedSize(originalWidth, originalHeight, maxEdge, maxTokens); return new RelativeCoordinate(x / resized.width(), y / resized.height()); } // To map back to your original image's pixel space, multiply by the original // dimensions: a table corner returned at (462, 653.5) on the resized A4 page // is (relative.x() * 1075, relative.y() * 1520) = (537.5, 760) on the // 1075x1520 original. ``` ```php PHP // This helper calls resizedSize() from the resize example on this page. /** * Map a pixel coordinate returned by Claude to relative coordinates in * [0, 1], as [x, y]. Pass the dimensions of the image you uploaded, and the * same tier limits used for resizedSize. */ function toRelativeCoordinates( float $x, float $y, int $originalWidth, int $originalHeight, int $maxEdge = 1568, int $maxTokens = 1568, ): array { [$resizedW, $resizedH] = resizedSize($originalWidth, $originalHeight, $maxEdge, $maxTokens); return [$x / $resizedW, $y / $resizedH]; } // A table corner Claude returns at (462, 653.5) on the resized A4 page maps // back onto the 1075x1520 original like this: [$relX, $relY] = toRelativeCoordinates(462, 653.5, 1075, 1520); echo '(' . $relX * 1075 . ', ' . $relY * 1520 . ")\n"; // (537.5, 760) ``` ```ruby Ruby # This helper calls resized_size from the resize example on this page. # Map a pixel coordinate returned by Claude to relative coordinates in # [0, 1], as [x, y]. Pass the dimensions of the image you uploaded, and the # same tier limits used for resized_size. def to_relative_coordinates( x, y, original_width, original_height, max_edge = 1568, max_tokens = 1568 ) resized_w, resized_h = resized_size(original_width, original_height, max_edge, max_tokens) [x.fdiv(resized_w), y.fdiv(resized_h)] end # A table corner Claude returns at (462, 653.5) on the resized A4 page maps # back onto the 1075x1520 original like this: rel_x, rel_y = to_relative_coordinates(462, 653.5, 1075, 1520) p [rel_x * 1075, rel_y * 1520] # => [537.5, 760.0] ``` Padding is applied only to the bottom and right edges, so the origin doesn't shift and a per-axis linear rescale is sufficient. Clamp returned coordinates to the resized dimensions before rescaling, so a point slightly outside the image can't map outside your original. The relative coordinates multiply against whatever surface you act on: the original image, a full-resolution scan, or a screen. When you act on a screen and screenshot pixels differ from logical coordinates (HiDPI displays), also divide by the display scale factor. The [Computer use tool's scaling guidance](https://platform.claude.com/docs/en/agents-and-tools/tool-use/computer-use-tool#handle-coordinate-scaling-for-higher-resolutions) covers that pattern. ## Next steps Agent Skills are modular capabilities that extend Claude's functionality. Each Skill packages instructions, metadata, and optional resources (scripts, templates) that Claude uses automatically when relevant. Give Claude screenshot, mouse, and keyboard control of a desktop environment with the computer use tool. Process PDFs with Claude. Extract text, analyze charts, and understand visual content from your documents. Count the tokens in a message before you send it to Claude. Use token counts to manage rate limits and costs, make model routing decisions, and fit prompts to a target length. --- title: Vision url: https://platform.claude.com/docs/en/build-with-claude/vision description: Claude's vision capabilities allow it to understand and analyze images, opening up exciting possibilities for multimodal interaction. --- This guide describes how to send images to Claude, the limits and costs that apply, and where to find guidance for [coordinate-based workflows](https://platform.claude.com/docs/en/build-with-claude/vision-coordinates). *** ## Send images to Claude Use Claude's vision capabilities through: * [claude.ai](https://claude.ai/). Upload an image like you would a file, or drag and drop an image directly into the chat window. * The [Workbench](https://platform.claude.com/playground) in the Claude Console. Add images directly to any User message block. * API request. See the following examples. On the API, provide images to Claude as `image` content blocks using one of three source types: 1. A base64-encoded image embedded in the request body 2. A URL reference to an image hosted online 3. A `file_id` returned by the [Files API](https://platform.claude.com/docs/en/build-with-claude/files) (upload once, reference many times) On Amazon Bedrock and Google Cloud, only base64-encoded sources are currently available. Just as [placing long documents before your query](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-prompting-best-practices#long-context-prompting) improves results in text prompts, Claude works best when images come before text. Images placed after text or interpolated with text still perform well, but if your use case allows it, prefer an image-then-text structure. ### Base64-encoded image example ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d @- < { new ContentBlockParam(new ImageBlockParam( new ImageBlockParamSource(new Base64ImageSource() { Data = imageData, MediaType = MediaType.ImagePng, }) )), new ContentBlockParam(new TextBlockParam("Describe this image.")), }), } ] }); Console.WriteLine(message); ``` ```go Go client := anthropic.NewClient() imageData := "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC" message, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage( anthropic.NewImageBlockBase64("image/png", imageData), anthropic.NewTextBlock("Describe this image."), ), }, }) if err != nil { log.Fatal(err) } fmt.Println(message) ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); String imageData = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC"; List contentBlockParams = List.of( ContentBlockParam.ofImage( ImageBlockParam.builder() .source( Base64ImageSource.builder() .mediaType(Base64ImageSource.MediaType.IMAGE_PNG) .data(imageData) .build() ) .build() ), ContentBlockParam.ofText(TextBlockParam.builder().text("Describe this image.").build()) ); Message message = client .messages() .create( MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .addUserMessageOfBlockParams(contentBlockParams) .build() ); IO.println(message); ``` ```php PHP $client = new Client(); $imageData = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC"; $message = $client->messages->create( maxTokens: 1024, messages: [ [ 'role' => 'user', 'content' => [ [ 'type' => 'image', 'source' => [ 'type' => 'base64', 'media_type' => 'image/png', 'data' => $imageData, ], ], ['type' => 'text', 'text' => 'Describe this image.'], ], ], ], model: 'claude-opus-5', ); echo json_encode($message, JSON_PRETTY_PRINT), PHP_EOL; ``` ```ruby Ruby client = Anthropic::Client.new image_data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC" message = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: [ { type: "image", source: { type: "base64", media_type: "image/png", data: image_data } }, { type: "text", text: "Describe this image." } ] } ] ) puts message ``` ### URL-based image example ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "messages": [ { "role": "user", "content": [ { "type": "image", "source": { "type": "url", "url": "https://platform.claude.com/docs/images/vision-example.jpg" } }, { "type": "text", "text": "Describe this image." } ] } ] }' ``` ```bash CLI ant messages create <<'YAML' model: claude-opus-5 max_tokens: 1024 messages: - role: user content: - type: image source: type: url url: https://platform.claude.com/docs/images/vision-example.jpg - type: text text: Describe this image. YAML ``` ```python Python client = anthropic.Anthropic() message = client.messages.create( model="claude-opus-5", max_tokens=1024, messages=[ { "role": "user", "content": [ { "type": "image", "source": { "type": "url", "url": "https://platform.claude.com/docs/images/vision-example.jpg", }, }, {"type": "text", "text": "Describe this image."}, ], } ], ) print(message) ``` ```typescript TypeScript const anthropic = new Anthropic(); const message = await anthropic.messages.create({ model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: [ { type: "image", source: { type: "url", url: "https://platform.claude.com/docs/images/vision-example.jpg" } }, { type: "text", text: "Describe this image." } ] } ] }); console.log(message); ``` ```csharp C# using System.Collections.Generic; using Anthropic; using Anthropic.Models.Messages; AnthropicClient client = new(); var message = await client.Messages.Create(new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [ new() { Role = Role.User, Content = new MessageParamContent(new List { new ContentBlockParam(new ImageBlockParam( new ImageBlockParamSource(new UrlImageSource() { Url = "https://platform.claude.com/docs/images/vision-example.jpg", }) )), new ContentBlockParam(new TextBlockParam("Describe this image.")), }), } ] }); Console.WriteLine(message); ``` ```go Go client := anthropic.NewClient() message, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage( anthropic.NewImageBlock(anthropic.URLImageSourceParam{ URL: "https://platform.claude.com/docs/images/vision-example.jpg", }), anthropic.NewTextBlock("Describe this image."), ), }, }) if err != nil { log.Fatal(err) } fmt.Println(message) ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); List contentBlockParams = List.of( ContentBlockParam.ofImage( ImageBlockParam.builder() .source( UrlImageSource.builder() .url("https://platform.claude.com/docs/images/vision-example.jpg") .build() ) .build() ), ContentBlockParam.ofText(TextBlockParam.builder().text("Describe this image.").build()) ); Message message = client .messages() .create( MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .addUserMessageOfBlockParams(contentBlockParams) .build() ); System.out.println(message); ``` ```php PHP $client = new Client(); $message = $client->messages->create( maxTokens: 1024, messages: [ [ 'role' => 'user', 'content' => [ [ 'type' => 'image', 'source' => [ 'type' => 'url', 'url' => 'https://platform.claude.com/docs/images/vision-example.jpg', ], ], ['type' => 'text', 'text' => 'Describe this image.'], ], ], ], model: 'claude-opus-5', ); echo json_encode($message, JSON_PRETTY_PRINT), PHP_EOL; ``` ```ruby Ruby client = Anthropic::Client.new message = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: [ { type: "image", source: { type: "url", url: "https://platform.claude.com/docs/images/vision-example.jpg" } }, { type: "text", text: "Describe this image." } ] } ] ) puts message ``` ### Files API image example For images you'll use repeatedly or when you want to avoid encoding overhead, use the [Files API](https://platform.claude.com/docs/en/build-with-claude/files). Upload the image once, then reference the returned `file_id` in subsequent messages instead of resending base64 data. In multi-turn conversations and agentic workflows, each request resends the full conversation history. If images are base64-encoded, the full image bytes are included in the payload on every turn, which can significantly increase request size and latency as the conversation grows. Uploading images to the Files API and referencing them by `file_id` keeps request payloads small regardless of how many images accumulate in the conversation history. ```bash cURL # First, upload your image to the Files API FILE_ID=$(curl -sS -X POST https://api.anthropic.com/v1/files \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: files-api-2025-04-14" \ -F "file=@vision-example.jpg" | jq -r '.id') # Then use the returned file_id in your message curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: files-api-2025-04-14" \ -H "content-type: application/json" \ -d @- <beta->files->upload( file: fopen('vision-example.jpg', 'r'), ); // Use the uploaded file in a message $message = $client->beta->messages->create( maxTokens: 1024, messages: [ [ 'role' => 'user', 'content' => [ [ 'type' => 'image', 'source' => ['type' => 'file', 'file_id' => $fileUpload->id], ], ['type' => 'text', 'text' => 'Describe this image.'], ], ], ], model: 'claude-opus-5', betas: ['files-api-2025-04-14'], ); echo json_encode($message, JSON_PRETTY_PRINT), PHP_EOL; ``` ```ruby Ruby client = Anthropic::Client.new # Upload the image file file_upload = client.beta.files.upload( file: File.open("vision-example.jpg", "rb") ) # Use the uploaded file in a message message = client.beta.messages.create( model: "claude-opus-5", max_tokens: 1024, betas: ["files-api-2025-04-14"], messages: [ { role: "user", content: [ { type: "image", source: { type: "file", file_id: file_upload.id } }, { type: "text", text: "Describe this image." } ] } ] ) puts message.content ``` See [Messages API examples](https://platform.claude.com/docs/en/api/messages/create) for more example code and parameter details. ### Multiple images You can include multiple images in a single request, and Claude analyzes them jointly. This is useful for comparing images, asking about differences, or working with a sequence such as pages of a document. When sending several images, introduce each one with a short text label (`Image 1:`, `Image 2:`, and so on) so you can refer to them by name in your prompt and in follow-up turns. ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Image 1:" }, { "type": "image", "source": { "type": "base64", "media_type": "image/png", "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC" } }, { "type": "text", "text": "Image 2:" }, { "type": "image", "source": { "type": "base64", "media_type": "image/png", "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGNgYPgPAAEDAQAIicLsAAAAAElFTkSuQmCC" } }, { "type": "text", "text": "How are these images different?" } ] } ] }' ``` ```bash CLI ant messages create <<'YAML' model: claude-opus-5 max_tokens: 1024 messages: - role: user content: - type: text text: "Image 1:" - type: image source: type: base64 media_type: image/png data: iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC - type: text text: "Image 2:" - type: image source: type: base64 media_type: image/png data: iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGNgYPgPAAEDAQAIicLsAAAAAElFTkSuQmCC - type: text text: How are these images different? YAML ``` ```python Python image1_data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC" image2_data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGNgYPgPAAEDAQAIicLsAAAAAElFTkSuQmCC" client = anthropic.Anthropic() message = client.messages.create( model="claude-opus-5", max_tokens=1024, messages=[ { "role": "user", "content": [ {"type": "text", "text": "Image 1:"}, { "type": "image", "source": { "type": "base64", "media_type": "image/png", "data": image1_data, }, }, {"type": "text", "text": "Image 2:"}, { "type": "image", "source": { "type": "base64", "media_type": "image/png", "data": image2_data, }, }, {"type": "text", "text": "How are these images different?"}, ], } ], ) print(message) ``` ```typescript TypeScript const anthropic = new Anthropic(); const image1Data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC"; const image2Data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGNgYPgPAAEDAQAIicLsAAAAAElFTkSuQmCC"; const message = await anthropic.messages.create({ model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: [ { type: "text", text: "Image 1:" }, { type: "image", source: { type: "base64", media_type: "image/png", data: image1Data } }, { type: "text", text: "Image 2:" }, { type: "image", source: { type: "base64", media_type: "image/png", data: image2Data } }, { type: "text", text: "How are these images different?" } ] } ] }); console.log(message); ``` ```csharp C# AnthropicClient client = new(); string image1Data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC"; string image2Data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGNgYPgPAAEDAQAIicLsAAAAAElFTkSuQmCC"; var message = await client.Messages.Create(new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [ new() { Role = Role.User, Content = new MessageParamContent(new List { new ContentBlockParam(new TextBlockParam("Image 1:")), new ContentBlockParam(new ImageBlockParam( new ImageBlockParamSource(new Base64ImageSource() { Data = image1Data, MediaType = MediaType.ImagePng, }) )), new ContentBlockParam(new TextBlockParam("Image 2:")), new ContentBlockParam(new ImageBlockParam( new ImageBlockParamSource(new Base64ImageSource() { Data = image2Data, MediaType = MediaType.ImagePng, }) )), new ContentBlockParam(new TextBlockParam("How are these images different?")), }), } ] }); Console.WriteLine(message); ``` ```go Go client := anthropic.NewClient() image1Data := "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC" image2Data := "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGNgYPgPAAEDAQAIicLsAAAAAElFTkSuQmCC" message, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage( anthropic.NewTextBlock("Image 1:"), anthropic.NewImageBlockBase64("image/png", image1Data), anthropic.NewTextBlock("Image 2:"), anthropic.NewImageBlockBase64("image/png", image2Data), anthropic.NewTextBlock("How are these images different?"), ), }, }) if err != nil { log.Fatal(err) } fmt.Println(message) ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); String image1Data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC"; String image2Data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGNgYPgPAAEDAQAIicLsAAAAAElFTkSuQmCC"; List contentBlockParams = List.of( ContentBlockParam.ofText(TextBlockParam.builder().text("Image 1:").build()), ContentBlockParam.ofImage( ImageBlockParam.builder() .source( Base64ImageSource.builder() .mediaType(Base64ImageSource.MediaType.IMAGE_PNG) .data(image1Data) .build() ) .build() ), ContentBlockParam.ofText(TextBlockParam.builder().text("Image 2:").build()), ContentBlockParam.ofImage( ImageBlockParam.builder() .source( Base64ImageSource.builder() .mediaType(Base64ImageSource.MediaType.IMAGE_PNG) .data(image2Data) .build() ) .build() ), ContentBlockParam.ofText( TextBlockParam.builder().text("How are these images different?").build() ) ); Message message = client .messages() .create( MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .addUserMessageOfBlockParams(contentBlockParams) .build() ); IO.println(message); ``` ```php PHP $client = new Client(); $image1Data = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC'; $image2Data = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGNgYPgPAAEDAQAIicLsAAAAAElFTkSuQmCC'; $message = $client->messages->create( maxTokens: 1024, messages: [ [ 'role' => 'user', 'content' => [ ['type' => 'text', 'text' => 'Image 1:'], [ 'type' => 'image', 'source' => [ 'type' => 'base64', 'media_type' => 'image/png', 'data' => $image1Data, ], ], ['type' => 'text', 'text' => 'Image 2:'], [ 'type' => 'image', 'source' => [ 'type' => 'base64', 'media_type' => 'image/png', 'data' => $image2Data, ], ], ['type' => 'text', 'text' => 'How are these images different?'], ], ], ], model: 'claude-opus-5', ); echo $message; ``` ```ruby Ruby client = Anthropic::Client.new image1_data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC" image2_data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGNgYPgPAAEDAQAIicLsAAAAAElFTkSuQmCC" message = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: [ { type: "text", text: "Image 1:" }, { type: "image", source: { type: "base64", media_type: "image/png", data: image1_data } }, { type: "text", text: "Image 2:" }, { type: "image", source: { type: "base64", media_type: "image/png", data: image2_data } }, { type: "text", text: "How are these images different?" } ] } ] ) puts message ``` In a multi-turn conversation, add new images in later `user` turns the same way. Claude has access to every image from earlier turns, so follow-up questions such as "Are these similar to the first two?" work without including the earlier images again in the new turn's content. *** ## Image limits and costs ### Request limits The maximum number of images per message or request is: * 20 per message on [claude.ai](https://claude.ai/). * 100 per request on the API, for models with a 200k-token context window. * 600 per request on the API, for all other models. The maximum dimensions per image are 8000x8000 px. If a single API request contains more than 20 images, a stricter per-image dimension limit applies. On Amazon Bedrock and Google Cloud, document blocks such as PDFs also count toward this threshold. Images exceeding the stricter limit are rejected with an `invalid_request_error` whose message references "many-image requests" and states the current limit in pixels. To stay under the limit on all platforms, either resize each image so that neither dimension exceeds 2000 px, or keep the request to 20 or fewer image and document blocks. The maximum size per image is: * 10 MB (base64-encoded) when using the Claude API directly. * 5 MB (base64-encoded) on Amazon Bedrock and Google Cloud. * 10 MB on [claude.ai](https://claude.ai/). Although the API supports up to 600 images per request, [request size limits](https://platform.claude.com/docs/en/api/overview#request-size-limits) (32 MB for standard endpoints; lower on some partner-operated platforms, for example, Amazon Bedrock and Google Cloud) can be reached first. For many images, consider uploading with the [Files API](https://platform.claude.com/docs/en/build-with-claude/vision#files-api-image-example) and referencing by `file_id` to keep request payloads small. Even when using the Files API, requests with many large images can fail before reaching the 600-image count. Reduce image dimensions or file sizes (for example, by downsampling) before uploading (see [Resolution and token cost](https://platform.claude.com/docs/en/build-with-claude/vision#evaluate-image-size)). ### Supported formats Claude supports JPEG, PNG, GIF, and WebP images (`image/jpeg`, `image/png`, `image/gif`, `image/webp`). Animations are unsupported, and only the first frame is used. ### Resolution and token cost Claude views images in patches instead of pixels. Each patch is a 28×28-pixel block of the image, referred to as a visual token. An image, therefore, costs `⌈width / 28⌉ × ⌈height / 28⌉` visual tokens. Each model has a maximum native image resolution, expressed as a long-edge limit and a visual-token limit. Images larger than either limit are downscaled before processing; see [How Claude resizes and pads images](https://platform.claude.com/docs/en/build-with-claude/vision-coordinates#how-claude-resizes-and-pads-images) for the exact rule. | Resolution tier | Models | Max long edge | Max visual tokens | | --------------- | --------------------------- | ------------- | ----------------- | | High-resolution | Claude 4.7 and later models | 2576 px | 4784 | | Standard | All other models | 1568 px | 1568 | High-resolution support is automatic on the listed models and requires no beta header or client-side opt-in. The following table shows the downsized resolution and visual-token cost for several image sizes on each tier: | Image size | Standard tier: downsized to | Standard tier: tokens | High-resolution tier: downsized to | High-resolution tier: tokens | | ------------------------------ | --------------------------- | --------------------- | ---------------------------------- | ---------------------------- | | 200x200 px (0.04 megapixels) | Not resized | 64 | Not resized | 64 | | 1000x1000 px (1 megapixel) | Not resized | 1296 | Not resized | 1296 | | 1092x1092 px (1.19 megapixels) | Not resized | 1521 | Not resized | 1521 | | 1920x1080 px (2.07 megapixels) | 1456x819 px | 1560 | Not resized | 2691 | | 2000x1500 px (3 megapixels) | 1269x952 px | 1564 | Not resized | 3888 | | 3840x2160 px (8.29 megapixels) | 1456x819 px | 1560 | 2576x1449 px | 4784 | When an image is downsized, Claude scales it to the largest size that fits the tier's limits while preserving its aspect ratio. This caps the token cost. For the precise rule and a reference implementation, see [How Claude resizes and pads images](https://platform.claude.com/docs/en/build-with-claude/vision-coordinates#how-claude-resizes-and-pads-images). To estimate cost, multiply the token count by the [per-token price of the model](https://claude.com/pricing) you're using. For example, at Claude Haiku 4.5's $1 USD per million input tokens (standard tier), the 1000×1000 image costs about $1.30 USD per thousand images. At Claude Opus 5's $5 USD per million (high-resolution tier), the same image costs about $6.48 USD per thousand and the 4K image about $23.92 USD per thousand. High-resolution images can use up to roughly three times more visual tokens than the same image on a standard-tier model. If you don't need the additional fidelity that high resolution provides for computer use, screenshot understanding, and dense documents, downsample images before sending to control token costs. To minimize latency and to simplify [coordinate-based workflows](https://platform.claude.com/docs/en/build-with-claude/vision-coordinates), prefer resizing images before uploading them. ### Image quality guidance When providing images to Claude, keep the following in mind for best results: * **Image clarity:** Ensure images are clear and not too blurry or pixelated. * **Text:** If the image contains important text, make sure it's legible and not too small. Avoid cropping out key visual context solely to enlarge the text. * **Resizing:** Take into account that your image might be resized if it is too large (see [Resolution and token cost](https://platform.claude.com/docs/en/build-with-claude/vision#evaluate-image-size)); this might, for example, make text less legible. Consider pre-resizing your images, cropping them, or both. * **Image compression:** Compressing images before sending them, using a lossy format such as JPEG or WebP (lossy mode), can reduce latency by reducing the size of requests. However, this can introduce artifacts that are detrimental to model performance, especially when multiple compression passes are applied. For example, heavy JPEG compression can make text difficult to read. Confirm your compression settings are appropriate for the task by inspecting the actual images sent to the API. *** ## Coordinates and bounding boxes For bounding boxes, points, and pixel coordinates, see [Coordinates and bounding boxes](https://platform.claude.com/docs/en/build-with-claude/vision-coordinates). Claude returns absolute pixel coordinates relative to the image it sees after resizing; that guide covers how Claude resizes and pads images and how to pre-resize or rescale so coordinates line up with your original image. *** ## Limitations Although Claude's image understanding capabilities are cutting-edge, there are some limitations to be aware of: * **People identification:** Claude [cannot be used](https://www.anthropic.com/legal/aup) to name people in images and refuses to do so. * **Accuracy:** Claude might hallucinate or make mistakes when interpreting low-quality, rotated, or very small images under 200 pixels. * **Spatial reasoning:** Claude's coordinate and localization outputs are approximate. Follow the guidance in [Coordinates and bounding boxes](https://platform.claude.com/docs/en/build-with-claude/vision-coordinates) and verify outputs before relying on them. * **Counting:** Claude can give approximate counts of objects in an image but might not always be precisely accurate, especially with large numbers of small objects. * **AI-generated images:** Claude cannot determine whether an image is AI-generated and might be incorrect if asked. Do not rely on it to detect fake or synthetic images. * **Inappropriate content:** Claude does not process inappropriate or explicit images that violate the [Acceptable Use Policy](https://www.anthropic.com/legal/aup). * **Healthcare applications:** Although Claude can analyze general medical images, it is not designed to interpret complex diagnostic scans such as CTs or MRIs. Claude's outputs should not be considered a substitute for professional medical advice or diagnosis. Always carefully review and verify Claude's image interpretations, especially for high-stakes use cases. Do not use Claude for tasks requiring perfect precision or sensitive image analysis without human oversight. *** ## FAQ JPEG, PNG, GIF, and WebP. See [Supported formats](https://platform.claude.com/docs/en/build-with-claude/vision#supported-formats). Yes. Use the `url` source type instead of `base64` in the `image` content block. See the [URL-based image example](https://platform.claude.com/docs/en/build-with-claude/vision#url-based-image-example). Yes. See [Request limits](https://platform.claude.com/docs/en/build-with-claude/vision#request-limits) for per-image and overall request size limits across the Claude API, Amazon Bedrock, Google Cloud, and claude.ai. Up to 600 per API request (100 for models with a 200k-token context window) and 20 per turn on claude.ai. See [Request limits](https://platform.claude.com/docs/en/build-with-claude/vision#request-limits) for details and the lower per-image dimension limit that applies above 20 images. No, Claude does not parse or receive any metadata from images passed to it. No. Image uploads are ephemeral and not stored beyond the duration of the API request. Uploaded images are automatically deleted after they have been processed. Refer to the Anthropic privacy policy page for information on how uploaded images and other data are handled. Anthropic does not use uploaded images to train models. If Claude's image interpretation seems incorrect: 1. Ensure the image is clear, high-quality, and correctly oriented. 2. Try prompt engineering techniques to improve results. 3. If the issue persists, flag the output in claude.ai (thumbs up/down) or contact the [support team](https://support.claude.com/). Your feedback helps improve Claude! No, Claude is an image understanding model only. It can interpret and analyze images, but it cannot generate, produce, edit, manipulate, or create images. *** ## Next steps Get tips and best-practice techniques for tasks such as interpreting charts and extracting content from forms. See the Messages API documentation, including example API calls involving images. ### Skills --- title: Agent Skills url: https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview description: Agent Skills are modular capabilities that extend Claude's functionality. Each Skill packages instructions, metadata, and optional resources (scripts, templates) that Claude uses automatically when relevant. --- For how zero data retention (ZDR) applies to this feature, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention). ## Why use Skills Skills are reusable, filesystem-based resources that give Claude domain-specific expertise: workflows, context, and best practices that turn a general-purpose agent into a specialist. Unlike prompts (conversation-level instructions for one-off tasks), Skills load on demand, so you don't have to repeat the same guidance across conversations. **Key benefits:** * **Specialize Claude:** Tailor capabilities for domain-specific tasks * **Reduce repetition:** Create once, use automatically * **Compose capabilities:** Combine Skills for complex, multistep tasks For more on the architecture and real-world applications of Agent Skills, see the engineering blog post [Equipping agents for the real world with Agent Skills](https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills). ## Using Skills Anthropic provides pre-built Agent Skills for common document tasks (PowerPoint, Excel, Word, PDF), and you can create your own custom Skills. Both work the same way: once a Skill is available in your environment, Claude uses it automatically when relevant to your request. **Pre-built Agent Skills** are available on claude.ai, the Claude API, [Claude Platform on AWS](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws), and [Microsoft Foundry](https://platform.claude.com/docs/en/build-with-claude/claude-in-microsoft-foundry). On Microsoft Foundry, Agent Skills require a [Hosted on Anthropic deployment](https://platform.claude.com/docs/en/build-with-claude/claude-in-microsoft-foundry#additional-features-not-supported-when-hosted-on-azure). See [Available Skills](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview#available-skills) for the complete list. **Custom Skills** let you package domain expertise and organizational knowledge. They're available across Claude's products: create them in Claude Code, upload them through the Claude API, or add them in claude.ai settings. On [Claude Platform on AWS](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws) and [Microsoft Foundry](https://platform.claude.com/docs/en/build-with-claude/claude-in-microsoft-foundry), upload custom Skills through the Skills API. **Get started:** * For pre-built Agent Skills: See the [quickstart tutorial](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/quickstart) to start using PowerPoint, Excel, Word, and PDF Skills in the API * For custom Skills: See the [Agent Skills Cookbook](https://platform.claude.com/cookbook/skills-notebooks-01-skills-introduction) to learn how to create your own Skills ## How Skills work Skills use Claude's VM environment to provide capabilities beyond what's possible with prompts alone. Claude operates in a virtual machine with filesystem access, allowing Skills to exist as directories containing instructions, executable code, and reference materials, organized like an onboarding guide you'd create for a new team member. This filesystem-based architecture enables **progressive disclosure:** Claude loads information in stages as needed, rather than consuming context upfront. Skills can contain three types of content, each loaded at a different time: ### Level 1: Metadata (always loaded) The Skill's YAML frontmatter provides discovery information: ```yaml --- name: pdf-processing description: Extract text and tables from PDF files, fill forms, merge documents. Use when working with PDF files or when the user mentions PDFs, forms, or document extraction. --- ``` Claude loads this metadata at startup and includes it in the system prompt. The `description` is what Claude matches your request against when determining whether to trigger the Skill, so it must say both what the Skill does and when to use it. This lightweight approach means you can install many Skills without context penalty: until a Skill is triggered, only its name and description occupy context. ### Level 2: Instructions (loaded when triggered) The main body of SKILL.md contains procedural knowledge: workflows, best practices, and guidance: ````markdown # PDF Processing ## Quick start Use pdfplumber to extract text from PDFs: ```python import pdfplumber with pdfplumber.open("document.pdf") as pdf: text = pdf.pages[0].extract_text() ``` For advanced form filling, see [FORMS.md](FORMS.md). ```` When you request something that matches a Skill's description, Claude reads SKILL.md from the filesystem using bash. Only then does this content enter the context window. ### Level 3: Resources and code (loaded as needed) Skills can bundle additional materials: * `pdf-processing/` * `SKILL.md` (main instructions) * `FORMS.md` (form-filling guide) * `REFERENCE.md` (detailed API reference) * `scripts/` * `fill_form.py` (utility script) **Instructions:** Additional markdown files (FORMS.md, REFERENCE.md) containing specialized guidance and workflows **Code:** Executable scripts (fill\_form.py, validate.py) that Claude runs using bash, providing deterministic operations without loading their code into context **Resources:** Reference materials such as database schemas, API documentation, templates, or examples Claude accesses these files only when referenced. The filesystem model means each content type has different strengths: instructions for flexible guidance, code for reliability, resources for factual lookup. | Level | When loaded | Token cost | Content | | ------------------------- | ----------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------- | | **Level 1: Metadata** | Always (at startup) | \~100 tokens per Skill | `name` and `description` from YAML frontmatter | | **Level 2: Instructions** | When Skill is triggered | Under 5k tokens | SKILL.md body with instructions and guidance | | **Level 3+: Resources** | As needed | None until accessed | Bundled files. Reference files load into context when read. Scripts run through bash, and only their output enters context | Progressive disclosure ensures only relevant content occupies the context window at any given time. ### The Skills architecture Skills run in a code execution environment where Claude has filesystem access, bash commands, and code execution capabilities. Skills exist as directories on a virtual machine, and Claude interacts with them using the same bash commands you'd use to navigate files on your computer. ![Agent Skills Architecture - showing how Skills integrate with the agent's configuration and virtual machine](https://platform.claude.com/docs/images/agent-skills-architecture.png) **How Claude accesses Skill content:** When a Skill is triggered, Claude uses bash to read SKILL.md from the filesystem, bringing its instructions into the context window. If those instructions reference other files (such as FORMS.md or a database schema), Claude reads those files too using additional bash commands. When instructions mention executable scripts, Claude runs them through bash and receives only the output (the script code itself never enters context). **What this architecture enables:** * **On-demand file access:** Claude reads only the files each task needs. A Skill can include dozens of reference files, but if your task only needs the sales schema, that's the one file Claude loads. The rest stay on the filesystem and cost zero tokens. * **Efficient script execution:** When Claude runs `validate_form.py`, the script's code never loads into the context window. Only its output (such as "Validation passed" or a specific error message) consumes tokens, which makes scripts far more efficient than having Claude generate equivalent code on the fly. * **No practical limit on bundled content:** Files don't consume context until accessed, so Skills can include comprehensive API documentation, large datasets, or extensive examples. There's no context penalty for bundled content that isn't used. ### Example: Loading a PDF processing Skill Here's how Claude loads and uses the custom `pdf-processing` Skill from the earlier examples (not the pre-built `pdf` Skill): 1. **Startup:** System prompt includes: `pdf-processing - Extract text and tables from PDF files, fill forms, merge documents. Use when working with PDF files or when the user mentions PDFs, forms, or document extraction.` 2. **User request:** "Extract the text from this PDF and summarize it" 3. **Claude invokes:** `bash: cat pdf-processing/SKILL.md` → Instructions loaded into context 4. **Claude determines:** Form filling is not needed, so FORMS.md is not read 5. **Claude executes:** Uses instructions from SKILL.md to complete the task ![Skills loading into context window - showing the progressive loading of skill metadata and content](https://platform.claude.com/docs/images/agent-skills-context-window.png) ## Where Skills work Skills are available across Claude's agent products: Claude Platform on AWS and Microsoft Foundry inherit the same Skills behavior as the Claude API in all following sections. ### Claude API The Claude API supports both pre-built Agent Skills and custom Skills. Both work identically: specify the relevant `skill_id` in the `container` parameter along with the [code execution tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool). **Prerequisites:** Using Skills through the API requires the [code execution tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool), whose container Skills run in, and one beta header: * `skills-2025-10-02` - Enables Skills functionality Add a second header, `files-api-2025-04-14`, when you use the [Files API](https://platform.claude.com/docs/en/build-with-claude/files) to upload input files to the container or download files a Skill produces. Use pre-built Agent Skills by referencing their `skill_id` (`pptx`, `xlsx`, `docx`, or `pdf`), or create and upload your own through the Skills API (`/v1/skills` endpoints). Custom Skills are shared workspace-wide: all workspace members can access them. Skills on the API run in a sandboxed container with no network access and no runtime package installation. See [Limitations and constraints](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview#limitations-and-constraints) for details. To learn more, see [Using Agent Skills with the API](https://platform.claude.com/docs/en/build-with-claude/skills-guide). ### Claude Code [Claude Code](https://code.claude.com/docs/en/overview) supports custom Skills. The pre-built document Skills (PowerPoint, Excel, Word, PDF) are not available in Claude Code, though the open-source [Claude API skill](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/claude-api-skill) comes bundled with it. See the full list of [built-in commands and Skills](https://code.claude.com/docs/en/commands) that ship with Claude Code. **Custom Skills:** Create Skills as directories with SKILL.md files. Claude discovers and uses them automatically. Custom Skills in Claude Code are filesystem-based and don't require API uploads: place them in `~/.claude/skills/` (personal) or `.claude/skills/` (project). To learn more, see [Use Skills in Claude Code](https://code.claude.com/docs/en/skills). ### claude.ai [claude.ai](https://claude.ai) supports both pre-built Agent Skills and custom Skills. **Pre-built Agent Skills:** These Skills are active when you create documents. Claude uses them with no setup required. **Custom Skills:** Upload your own Skills as zip files through Settings > Features. Available on Pro, Max, Team, and Enterprise plans with [code execution enabled](https://support.claude.com/en/articles/12111783-create-and-edit-files-with-claude). Custom Skills are individual to each user. They are not shared organization-wide and cannot be centrally managed by admins. To learn more about using Skills in claude.ai, see the following resources in the Claude Help Center: * [What are Skills?](https://support.claude.com/en/articles/12512176-what-are-skills) * [Using Skills in Claude](https://support.claude.com/en/articles/12512180-using-skills-in-claude) * [How to create custom Skills](https://support.claude.com/en/articles/12512198-creating-custom-skills) * [Teach Claude your way of working using Skills](https://support.claude.com/en/articles/12580051-teach-claude-your-way-of-working-using-skills) ## Skill structure Every Skill requires a `SKILL.md` file with YAML frontmatter: ```markdown --- name: your-skill-name description: Brief description of what this Skill does and when to use it --- # Your Skill Name ## Instructions [Clear, step-by-step guidance for Claude to follow] ## Examples [Concrete examples of using this Skill] ``` **Required fields:** `name` and `description` **Field requirements:** `name`: * Maximum 64 characters * Must contain only lowercase letters, numbers, and hyphens * Cannot contain XML tags * Cannot contain reserved words: "anthropic", "claude" `description`: * Must be non-empty * Maximum 1024 characters * Cannot contain XML tags The `description` must include both what the Skill does and when Claude should use it. For complete authoring guidance, see [Skill authoring best practices](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices). ## Security considerations Use Skills only from trusted sources: those you created yourself or obtained from Anthropic. Skills give Claude new capabilities through instructions and code, which also means a malicious Skill can direct Claude to invoke tools or execute code in ways that don't match the Skill's stated purpose. If you must use a Skill from an untrusted or unknown source, exercise extreme caution and thoroughly audit it before use. Depending on what access Claude has when executing the Skill, malicious Skills could lead to data exfiltration, unauthorized system access, or other security risks. **Key security considerations:** * **Audit thoroughly:** Review all files bundled in the Skill: SKILL.md, scripts, images, and other resources. Look for unusual patterns such as unexpected network calls, file access patterns, or operations that don't match the Skill's stated purpose * **External sources are risky:** Skills that fetch data from external URLs pose particular risk, as fetched content may contain malicious instructions. Even trustworthy Skills can be compromised if their external dependencies change over time * **Tool misuse:** Malicious Skills can invoke tools (file operations, bash commands, code execution) in harmful ways * **Data exposure:** Skills with access to sensitive data could be designed to leak information to external systems * **Treat like installing software:** Be especially careful when integrating Skills into production systems with access to sensitive data or critical operations For organization-scale governance, vetting, and deployment guidance, see [Skills for enterprise](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/enterprise). ## Available Skills ### Pre-built Agent Skills The following pre-built Agent Skills are available for immediate use: * **PowerPoint (pptx):** Create presentations, edit slides, analyze presentation content * **Excel (xlsx):** Create spreadsheets, analyze data, generate reports with charts * **Word (docx):** Create documents, edit content, format text * **PDF (pdf):** Generate formatted PDF documents and reports These Skills are available on the Claude API, [Claude Platform on AWS](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws), [Microsoft Foundry](https://platform.claude.com/docs/en/build-with-claude/claude-in-microsoft-foundry), and claude.ai. See the [quickstart tutorial](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/quickstart) to start using them in the API. ### Open-source Skills Anthropic also publishes open-source Skills in the [skills repository](https://github.com/anthropics/skills): * **[Claude API skill](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/claude-api-skill):** Provides Claude with up-to-date API reference material, SDK documentation, and best practices for eight programming languages. Bundled with Claude Code and also available for installation from the skills repository. ### Custom Skills examples For complete examples of custom Skills, see the [Skills cookbook](https://platform.claude.com/cookbook/skills-notebooks-01-skills-introduction). ## Data retention Agent Skills is not covered by ZDR arrangements. Skill definitions and execution data are retained according to Anthropic's standard data retention policy. For ZDR eligibility across all features, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention). ## Limitations and constraints Claude Platform on AWS and Microsoft Foundry follow the same limitations as the Claude API in the following subsections. ### Cross-surface availability **Custom Skills do not sync across surfaces**. Skills uploaded to one surface are not automatically available on others: * Skills uploaded to claude.ai must be separately uploaded to the API * Skills uploaded through the API are not available on claude.ai * Claude Code Skills are filesystem-based and separate from both claude.ai and API Manage and upload Skills separately for each surface where you want to use them. ### Sharing scope Skills have different sharing models depending on where you use them: * **claude.ai:** Individual user only. Each team member must upload separately. * **Claude API:** Workspace-wide. All workspace members can access uploaded Skills. * **Claude Code:** Personal (`~/.claude/skills/`) or project-based (`.claude/skills/`). Can also be shared through Claude Code Plugins. claude.ai does not support centralized admin management or org-wide distribution of custom Skills. ### Runtime environment constraints The exact runtime environment available to your Skill depends on the product surface where you use it. * **claude.ai:** * **Varying network access:** Depending on user/admin settings, Skills may have full, partial, or no network access. For more details, see the [Create and Edit Files](https://support.claude.com/en/articles/12111783-create-and-edit-files-with-claude#h_6b7e833898) support article. * **Claude API:** * **No network access:** Skills cannot make external API calls or access the internet. * **No runtime package installation:** Only pre-installed packages are available. You cannot install new packages during execution. * **Pre-configured dependencies only:** Check the [Code execution tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool) documentation for the list of available packages. * **Claude Code:** * **Full network access:** Skills have the same network access as any other program on the user's computer. * **Global package installation discouraged:** Skills should only install packages locally to avoid interfering with the user's computer. Plan your Skills to work within these constraints. ## Next steps Learn how to use Agent Skills to create documents with the Claude API in under 10 minutes. Learn how to use Agent Skills to extend Claude's capabilities through the API. Create and manage custom Skills in Claude Code. Learn how to write effective Skills that Claude can discover and use successfully. --- title: Get started with Agent Skills in the API url: https://platform.claude.com/docs/en/agents-and-tools/agent-skills/quickstart description: Learn how to use Agent Skills to create documents with the Claude API in under 10 minutes. --- This tutorial shows you how to use Agent Skills to create a PowerPoint presentation. You'll learn how to enable Skills, make a request, and access the generated file. ## Prerequisites * A [Claude API key](https://platform.claude.com/settings/keys) or a logged-in [ant CLI](https://platform.claude.com/docs/en/cli-sdks-libraries/cli/authentication) * A [client SDK](https://platform.claude.com/docs/en/cli-sdks-libraries/overview) for your language, or `curl` and `jq` * Basic familiarity with making API requests ## Agent Skills overview Pre-built Agent Skills extend Claude's capabilities with specialized expertise for tasks such as creating documents, analyzing data, and processing files. Anthropic provides the following pre-built Agent Skills in the API: * **PowerPoint (pptx):** Create and edit presentations * **Excel (xlsx):** Create and analyze spreadsheets * **Word (docx):** Create and edit documents * **PDF (pdf):** Generate PDF documents To create custom Skills, see the [Agent Skills Cookbook](https://platform.claude.com/cookbook/skills-notebooks-01-skills-introduction) for examples of building your own Skills with domain-specific expertise. ## Step 1: List available Skills First, check what Skills are available. Use the Skills API to list all Anthropic-managed Skills. Each language tab is an excerpt from one continuous script, with any imports and client setup at the top: ```bash cURL # List Anthropic-managed Skills curl --fail-with-body -sS "https://api.anthropic.com/v1/skills?source=anthropic" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: skills-2025-10-02" | jq -r '.data[] | "\(.id): \(.display_title)"' ``` ```bash CLI # List Anthropic-managed Skills ant beta:skills list --source anthropic ``` ```python Python # List Anthropic-managed Skills skills = client.beta.skills.list(source="anthropic") for skill in skills.data: print(f"{skill.id}: {skill.display_title}") ``` ```typescript TypeScript // List Anthropic-managed Skills const skills = await client.beta.skills.list({ source: "anthropic" }); for (const skill of skills.data) { console.log(`${skill.id}: ${skill.display_title}`); } ``` ```csharp C# // List Anthropic-managed Skills var skills = await client.Beta.Skills.List(new SkillListParams { Source = "anthropic" }); foreach (var skill in skills.Items) { Console.WriteLine($"{skill.ID}: {skill.DisplayTitle}"); } ``` ```go Go // List Anthropic-managed Skills skills, err := client.Beta.Skills.List(ctx, anthropic.BetaSkillListParams{ Source: anthropic.String("anthropic"), }) if err != nil { panic(err) } for _, skill := range skills.Data { fmt.Printf("%s: %s\n", skill.ID, skill.DisplayTitle) } ``` ```java Java // List Anthropic-managed Skills SkillListPage skills = client.beta().skills().list( SkillListParams.builder().source("anthropic").build() ); for (SkillListResponse skill : skills.data()) { IO.println(skill.id() + ": " + skill.displayTitle().orElse("")); } ``` ```php PHP // List Anthropic-managed Skills $skills = $client->beta->skills->list(source: 'anthropic'); foreach ($skills->data as $skill) { echo "{$skill->id}: {$skill->displayTitle}\n"; } ``` ```ruby Ruby # List Anthropic-managed Skills skills = client.beta.skills.list(source: "anthropic") skills.data.each do |skill| puts "#{skill.id}: #{skill.display_title}" end ``` You see the following Skills: `pptx`, `xlsx`, `docx`, and `pdf`. This API returns each Skill's metadata: its name and description. Claude loads this metadata at startup to determine which Skills are available. This is the first level of **progressive disclosure**, where Claude discovers Skills without loading their full instructions yet. ## Step 2: Create a presentation Use the PowerPoint Skill to create a presentation about renewable energy. Specify Skills using the `container` parameter in the Messages API: ```bash cURL # Create a message with the PowerPoint Skill response=$( curl --fail-with-body -sS https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: skills-2025-10-02" \ -d @- <<'EOF' { "model": "claude-opus-5", "max_tokens": 16000, "container": { "skills": [{"type": "anthropic", "skill_id": "pptx", "version": "latest"}] }, "messages": [ {"role": "user", "content": "Create a presentation about renewable energy with 5 slides"} ], "tools": [{"type": "code_execution_20260521", "name": "code_execution"}] } EOF ) jq -r '"stop_reason=\(.stop_reason), blocks=\(.content | length)"' <<<"$response" ``` ```bash CLI # Create a message with the PowerPoint Skill response=$(ant beta:messages create --format json \ --beta skills-2025-10-02 <<'YAML' model: claude-opus-5 max_tokens: 16000 container: skills: - type: anthropic skill_id: pptx version: latest messages: - role: user content: Create a presentation about renewable energy with 5 slides tools: - type: code_execution_20260521 name: code_execution YAML ) jq -r '"stop_reason=\(.stop_reason), blocks=\(.content | length)"' <<<"$response" ``` ```python Python # Create a message with the PowerPoint Skill response = client.beta.messages.create( model="claude-opus-5", max_tokens=16000, betas=["skills-2025-10-02"], container={ "skills": [{"type": "anthropic", "skill_id": "pptx", "version": "latest"}] }, messages=[ { "role": "user", "content": "Create a presentation about renewable energy with 5 slides", } ], tools=[{"type": "code_execution_20260521", "name": "code_execution"}], ) print(f"stop_reason={response.stop_reason}, blocks={len(response.content)}") ``` ```typescript TypeScript // Create a message with the PowerPoint Skill const response = await client.beta.messages.create({ model: "claude-opus-5", max_tokens: 16000, betas: ["skills-2025-10-02"], container: { skills: [{ type: "anthropic", skill_id: "pptx", version: "latest" }], }, messages: [ { role: "user", content: "Create a presentation about renewable energy with 5 slides", }, ], tools: [{ type: "code_execution_20260521", name: "code_execution" }], }); console.log( `stop_reason=${response.stop_reason}, blocks=${response.content.length}`, ); ``` ```csharp C# // Create a message with the PowerPoint Skill var response = await client.Beta.Messages.Create(new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 16000, Betas = ["skills-2025-10-02"], Container = new BetaContainerParams { Skills = [ new BetaSkillParams { Type = BetaSkillParamsType.Anthropic, SkillID = "pptx", Version = "latest", }, ], }, Messages = [ new BetaMessageParam { Role = Role.User, Content = "Create a presentation about renewable energy with 5 slides", }, ], Tools = [new BetaCodeExecutionTool20260521()], }); Console.WriteLine($"stop_reason={response.StopReason?.Raw()}, blocks={response.Content.Count}"); ``` ```go Go // Create a message with the PowerPoint Skill response, err := client.Beta.Messages.New(ctx, anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 16000, Betas: []anthropic.AnthropicBeta{ anthropic.AnthropicBetaSkills2025_10_02, }, Container: anthropic.BetaMessageNewParamsContainerUnion{ OfContainers: &anthropic.BetaContainerParams{ Skills: []anthropic.BetaSkillParams{ { Type: anthropic.BetaSkillParamsTypeAnthropic, SkillID: "pptx", Version: anthropic.String("latest"), }, }, }, }, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage( anthropic.NewBetaTextBlock("Create a presentation about renewable energy with 5 slides"), ), }, Tools: []anthropic.BetaToolUnionParam{ {OfCodeExecutionTool20260521: &anthropic.BetaCodeExecutionTool20260521Param{}}, }, }) if err != nil { panic(err) } fmt.Printf("stop_reason=%s, blocks=%d\n", response.StopReason, len(response.Content)) ``` ```java Java // Create a message with the PowerPoint Skill BetaMessage response = client.beta().messages().create( MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(16000) .addBeta(AnthropicBeta.SKILLS_2025_10_02) .container( BetaContainerParams.builder() .addSkill( BetaSkillParams.builder() .type(BetaSkillParams.Type.ANTHROPIC) .skillId("pptx") .version("latest") .build() ) .build() ) .addUserMessage("Create a presentation about renewable energy with 5 slides") .addTool(BetaCodeExecutionTool20260521.builder().build()) .build() ); IO.println( "stop_reason=" + response.stopReason().orElse(null) + ", blocks=" + response.content().size() ); ``` ```php PHP // Create a message with the PowerPoint Skill $response = $client->beta->messages->create( model: 'claude-opus-5', maxTokens: 16000, betas: ['skills-2025-10-02'], container: [ 'skills' => [['type' => 'anthropic', 'skill_id' => 'pptx', 'version' => 'latest']], ], messages: [ [ 'role' => 'user', 'content' => 'Create a presentation about renewable energy with 5 slides', ], ], tools: [['type' => 'code_execution_20260521', 'name' => 'code_execution']], ); printf("stop_reason=%s, blocks=%d\n", $response->stopReason, count($response->content)); ``` ```ruby Ruby # Create a message with the PowerPoint Skill response = client.beta.messages.create( model: "claude-opus-5", max_tokens: 16_000, betas: ["skills-2025-10-02"], container: { skills: [{type: "anthropic", skill_id: "pptx", version: "latest"}] }, messages: [ { role: "user", content: "Create a presentation about renewable energy with 5 slides" } ], tools: [{type: "code_execution_20260521", name: "code_execution"}] ) puts "stop_reason=#{response.stop_reason}, blocks=#{response.content.length}" ``` The request includes the following parts: * **`model`:** A [model that supports the code execution tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool#model-compatibility) * **`container.skills`:** Specifies which Skills Claude can use * **`type: "anthropic"`:** Indicates this is an Anthropic-managed Skill * **`skill_id: "pptx"`:** The PowerPoint Skill identifier * **`version: "latest"`:** The Skill version set to the most recently published * **`tools`:** Enables code execution (required for Skills) * **Beta header:** `skills-2025-10-02` The examples on this page use the `code_execution_20260521` tool version, which is generally available and needs only the `skills-2025-10-02` beta header. The Step 3 code parses the result types that current tool versions return. Skills also work with older [code execution tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool) versions such as `code_execution_20250825`: any current code execution tool version satisfies the Skills requirement. If you use a different version, keep its tool `type` and any beta header consistent with the code execution tool page, and always include `skills-2025-10-02`. When you make this request, Claude automatically matches your task to the relevant Skill. Because you asked for a presentation, Claude determines the PowerPoint Skill is relevant and loads its full instructions: the second level of progressive disclosure. Then Claude runs the Skill's code to create your presentation. ## Step 3: Download the created file The presentation was created in the code execution container and saved as a file. The Step 2 `response` includes a file reference with a file ID. Extract the file ID and download the file with the Files API. The example saves it to your system temp directory: ```bash cURL # Extract the file ID. The code execution tool runs the Skill's code through # its Bash sub-tool, and generated files appear as bash_code_execution_output # items inside the bash_code_execution_tool_result block. file_id=$(jq -r ' last( .content[] | select(.type == "bash_code_execution_tool_result") | .content | select(.type == "bash_code_execution_result") | .content[].file_id ) // empty ' <<<"$response") if [[ -n "$file_id" ]]; then # Download the file and save it output_path="${TMPDIR:-/tmp}/renewable_energy.pptx" curl --fail-with-body -sS "https://api.anthropic.com/v1/files/$file_id/content" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: files-api-2025-04-14" \ -o "$output_path" echo "Presentation saved to $output_path" fi ``` ```bash CLI # Extract the file ID. The code execution tool runs the Skill's code through # its Bash sub-tool, and generated files appear as bash_code_execution_output # items inside the bash_code_execution_tool_result block. file_id=$(jq -r ' last( .content[] | select(.type == "bash_code_execution_tool_result") | .content | select(.type == "bash_code_execution_result") | .content[].file_id ) // empty ' <<<"$response") if [[ -n "$file_id" ]]; then # Download the file and save it output_path="${TMPDIR:-/tmp}/renewable_energy.pptx" ant beta:files download --file-id "$file_id" --output "$output_path" echo "Presentation saved to $output_path" fi ``` ```python Python # Extract the file ID. The code execution tool runs the Skill's code through # its Bash sub-tool, and generated files appear as bash_code_execution_output # items inside the bash_code_execution_tool_result block. file_id = None for block in response.content: if block.type == "bash_code_execution_tool_result": if block.content.type == "bash_code_execution_result": for output in block.content.content: file_id = output.file_id if file_id: # Download the file and save it output_path = Path(tempfile.gettempdir()) / "renewable_energy.pptx" file_content = client.beta.files.download(file_id=file_id) file_content.write_to_file(output_path) print(f"Presentation saved to {output_path}") ``` ```typescript TypeScript // Extract the file ID. The code execution tool runs the Skill's code through // its Bash sub-tool, and generated files appear as bash_code_execution_output // items inside the bash_code_execution_tool_result block. let fileId: string | undefined; for (const block of response.content) { if ( block.type === "bash_code_execution_tool_result" && block.content.type === "bash_code_execution_result" ) { for (const output of block.content.content) { fileId = output.file_id; } } } if (fileId) { // Download the file and save it const outputPath = path.join(os.tmpdir(), "renewable_energy.pptx"); const fileContent = await client.beta.files.download(fileId); await fs.writeFile(outputPath, Buffer.from(await fileContent.arrayBuffer())); console.log(`Presentation saved to ${outputPath}`); } ``` ```csharp C# // Extract the file ID. The code execution tool runs the Skill's code through // its Bash sub-tool, and generated files appear as bash_code_execution_output // items inside the bash_code_execution_tool_result block. string? fileId = null; foreach (var block in response.Content) { if (block.TryPickBashCodeExecutionToolResult(out var bashResult) && bashResult.Content.TryPickBetaBashCodeExecutionResultBlock(out var bashResultBlock)) { foreach (var output in bashResultBlock.Content) { fileId = output.FileID; } } } if (fileId is not null) { // Download the file and save it var outputPath = Path.Combine(Path.GetTempPath(), "renewable_energy.pptx"); using var download = await client.Beta.Files.Download(fileId); await using var source = await download.ReadAsStream(); await using var destination = File.Create(outputPath); await source.CopyToAsync(destination); Console.WriteLine($"Presentation saved to {outputPath}"); } ``` ```go Go // Extract the file ID. The code execution tool runs the Skill's code through // its Bash sub-tool, and generated files appear as bash_code_execution_output // items inside the bash_code_execution_tool_result block. var fileID string for _, block := range response.Content { switch result := block.AsAny().(type) { case anthropic.BetaBashCodeExecutionToolResultBlock: if result.Content.Type == "bash_code_execution_result" { for _, output := range result.Content.Content { fileID = output.FileID } } } } if fileID != "" { // Download the file and save it outputPath := filepath.Join(os.TempDir(), "renewable_energy.pptx") fileContent, err := client.Beta.Files.Download(ctx, fileID, anthropic.BetaFileDownloadParams{}) if err != nil { panic(err) } defer fileContent.Body.Close() outFile, err := os.Create(outputPath) if err != nil { panic(err) } defer outFile.Close() if _, err := io.Copy(outFile, fileContent.Body); err != nil { panic(err) } fmt.Printf("Presentation saved to %s\n", outputPath) } ``` ```java Java // Extract the file ID. The code execution tool runs the Skill's code through // its Bash sub-tool, and generated files appear as bash_code_execution_output // items inside the bash_code_execution_tool_result block. String fileId = null; for (BetaContentBlock block : response.content()) { if (block.isBashCodeExecutionToolResult()) { var content = block.asBashCodeExecutionToolResult().content(); if (content.isBetaBashCodeExecutionResultBlock()) { for (var output : content.asBetaBashCodeExecutionResultBlock().content()) { fileId = output.fileId(); } } } } if (fileId != null) { // Download the file and save it Path outputPath = Files.createTempFile("renewable_energy", ".pptx"); try (HttpResponse fileContent = client.beta().files().download(fileId)) { Files.copy(fileContent.body(), outputPath, StandardCopyOption.REPLACE_EXISTING); } IO.println("Presentation saved to " + outputPath); } ``` ```php PHP // Extract the file ID. The code execution tool runs the Skill's code through // its Bash sub-tool, and generated files appear as bash_code_execution_output // items inside the bash_code_execution_tool_result block. $fileId = null; foreach ($response->content as $block) { if ($block->type !== 'bash_code_execution_tool_result') { continue; } $resultBlock = $block->content; if ($resultBlock->type !== 'bash_code_execution_result') { continue; } foreach ($resultBlock->content as $output) { $fileId = $output->fileID; } } if ($fileId !== null) { // Download the file and save it $outputPath = sys_get_temp_dir() . '/renewable_energy.pptx'; $fileContent = $client->beta->files->download($fileId); file_put_contents($outputPath, $fileContent); echo "Presentation saved to {$outputPath}\n"; } ``` ```ruby Ruby # Extract the file ID. The code execution tool runs the Skill's code through # its Bash sub-tool, and generated files appear as bash_code_execution_output # items inside the bash_code_execution_tool_result block. file_id = nil response.content.each do |block| next unless block.type == :bash_code_execution_tool_result if block.content[:type].to_s == "bash_code_execution_result" Array(block.content[:content]).each { |output| file_id = output[:file_id] } end end if file_id # Download the file and save it output_path = File.join(Dir.tmpdir, "renewable_energy.pptx") file_content = client.beta.files.download(file_id) File.binwrite(output_path, file_content.read) puts "Presentation saved to #{output_path}" end ``` For complete details on working with generated files, see [Retrieve generated files](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool#retrieve-generated-files) in the code execution tool documentation. ## Try more examples Try these variations: ### Create a spreadsheet ```bash cURL curl --fail-with-body -sS https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: skills-2025-10-02" \ -d '{ "model": "claude-opus-5", "max_tokens": 16000, "container": { "skills": [{"type": "anthropic", "skill_id": "xlsx", "version": "latest"}] }, "messages": [ {"role": "user", "content": "Create a quarterly sales tracking spreadsheet with sample data"} ], "tools": [{"type": "code_execution_20260521", "name": "code_execution"}] }' | jq -r '"stop_reason=\(.stop_reason)"' ``` ```bash CLI ant beta:messages create --format json \ --beta skills-2025-10-02 <<'YAML' | jq -r '"stop_reason=\(.stop_reason)"' model: claude-opus-5 max_tokens: 16000 container: skills: - type: anthropic skill_id: xlsx version: latest messages: - role: user content: Create a quarterly sales tracking spreadsheet with sample data tools: - type: code_execution_20260521 name: code_execution YAML ``` ```python Python response = client.beta.messages.create( model="claude-opus-5", max_tokens=16000, betas=["skills-2025-10-02"], container={ "skills": [{"type": "anthropic", "skill_id": "xlsx", "version": "latest"}] }, messages=[ { "role": "user", "content": "Create a quarterly sales tracking spreadsheet with sample data", } ], tools=[{"type": "code_execution_20260521", "name": "code_execution"}], ) ``` ```typescript TypeScript const response = await client.beta.messages.create({ model: "claude-opus-5", max_tokens: 16000, betas: ["skills-2025-10-02"], container: { skills: [{ type: "anthropic", skill_id: "xlsx", version: "latest" }] }, messages: [ { role: "user", content: "Create a quarterly sales tracking spreadsheet with sample data" } ], tools: [{ type: "code_execution_20260521", name: "code_execution" }] }); ``` ```csharp C# var response = await client.Beta.Messages.Create( new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 16000, Betas = ["skills-2025-10-02"], Container = new BetaContainerParams { Skills = [ new BetaSkillParams { Type = BetaSkillParamsType.Anthropic, SkillID = "xlsx", Version = "latest", }, ], }, Messages = [ new BetaMessageParam { Role = Role.User, Content = "Create a quarterly sales tracking spreadsheet with sample data", }, ], Tools = [new BetaCodeExecutionTool20260521()], } ); ``` ```go Go response, err := client.Beta.Messages.New(context.Background(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 16000, Betas: []anthropic.AnthropicBeta{ anthropic.AnthropicBetaSkills2025_10_02, }, Container: anthropic.BetaMessageNewParamsContainerUnion{ OfContainers: &anthropic.BetaContainerParams{ Skills: []anthropic.BetaSkillParams{ { Type: anthropic.BetaSkillParamsTypeAnthropic, SkillID: "xlsx", Version: anthropic.String("latest"), }, }, }, }, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Create a quarterly sales tracking spreadsheet with sample data")), }, Tools: []anthropic.BetaToolUnionParam{ { OfCodeExecutionTool20260521: &anthropic.BetaCodeExecutionTool20260521Param{}, }, }, }) if err != nil { panic(err) } ``` ```java Java BetaMessage response = client.beta().messages().create( MessageCreateParams.builder() .model(CLAUDE_OPUS_5) .maxTokens(16000) .addBeta(AnthropicBeta.SKILLS_2025_10_02) .container( BetaContainerParams.builder() .addSkill( BetaSkillParams.builder() .type(ANTHROPIC) .skillId("xlsx") .version("latest") .build() ) .build() ) .addUserMessage("Create a quarterly sales tracking spreadsheet with sample data") .addTool(BetaCodeExecutionTool20260521.builder().build()) .build() ); ``` ```php PHP $response = $client->beta->messages->create( model: 'claude-opus-5', maxTokens: 16000, betas: ['skills-2025-10-02'], container: [ 'skills' => [ ['type' => 'anthropic', 'skill_id' => 'xlsx', 'version' => 'latest'], ], ], messages: [ [ 'role' => 'user', 'content' => 'Create a quarterly sales tracking spreadsheet with sample data', ], ], tools: [new BetaCodeExecutionTool20260521()], ); ``` ```ruby Ruby response = client.beta.messages.create( model: "claude-opus-5", max_tokens: 16_000, betas: ["skills-2025-10-02"], container: { skills: [{type: "anthropic", skill_id: "xlsx", version: "latest"}] }, messages: [ { role: "user", content: "Create a quarterly sales tracking spreadsheet with sample data" } ], tools: [{type: "code_execution_20260521", name: "code_execution"}] ) ``` ### Create a Word document ```bash cURL curl --fail-with-body -sS https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: skills-2025-10-02" \ -d '{ "model": "claude-opus-5", "max_tokens": 16000, "container": { "skills": [{"type": "anthropic", "skill_id": "docx", "version": "latest"}] }, "messages": [ {"role": "user", "content": "Write a 2-page report on the benefits of renewable energy"} ], "tools": [{"type": "code_execution_20260521", "name": "code_execution"}] }' | jq -r '"stop_reason=\(.stop_reason)"' ``` ```bash CLI ant beta:messages create --format json \ --beta skills-2025-10-02 <<'YAML' | jq -r '"stop_reason=\(.stop_reason)"' model: claude-opus-5 max_tokens: 16000 container: skills: - type: anthropic skill_id: docx version: latest messages: - role: user content: Write a 2-page report on the benefits of renewable energy tools: - type: code_execution_20260521 name: code_execution YAML ``` ```python Python response = client.beta.messages.create( model="claude-opus-5", max_tokens=16000, betas=["skills-2025-10-02"], container={ "skills": [{"type": "anthropic", "skill_id": "docx", "version": "latest"}] }, messages=[ { "role": "user", "content": "Write a 2-page report on the benefits of renewable energy", } ], tools=[{"type": "code_execution_20260521", "name": "code_execution"}], ) ``` ```typescript TypeScript const response = await client.beta.messages.create({ model: "claude-opus-5", max_tokens: 16000, betas: ["skills-2025-10-02"], container: { skills: [{ type: "anthropic", skill_id: "docx", version: "latest" }] }, messages: [ { role: "user", content: "Write a 2-page report on the benefits of renewable energy" } ], tools: [{ type: "code_execution_20260521", name: "code_execution" }] }); ``` ```csharp C# var response = await client.Beta.Messages.Create( new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 16000, Betas = ["skills-2025-10-02"], Container = new BetaContainerParams { Skills = [ new BetaSkillParams { Type = BetaSkillParamsType.Anthropic, SkillID = "docx", Version = "latest", }, ], }, Messages = [ new BetaMessageParam { Role = Role.User, Content = "Write a 2-page report on the benefits of renewable energy", }, ], Tools = [new BetaCodeExecutionTool20260521()], } ); ``` ```go Go response, err := client.Beta.Messages.New(context.Background(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 16000, Betas: []anthropic.AnthropicBeta{ anthropic.AnthropicBetaSkills2025_10_02, }, Container: anthropic.BetaMessageNewParamsContainerUnion{ OfContainers: &anthropic.BetaContainerParams{ Skills: []anthropic.BetaSkillParams{ { Type: anthropic.BetaSkillParamsTypeAnthropic, SkillID: "docx", Version: anthropic.String("latest"), }, }, }, }, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Write a 2-page report on the benefits of renewable energy")), }, Tools: []anthropic.BetaToolUnionParam{ { OfCodeExecutionTool20260521: &anthropic.BetaCodeExecutionTool20260521Param{}, }, }, }) if err != nil { panic(err) } ``` ```java Java BetaMessage response = client.beta().messages().create( MessageCreateParams.builder() .model(CLAUDE_OPUS_5) .maxTokens(16000) .addBeta(AnthropicBeta.SKILLS_2025_10_02) .container( BetaContainerParams.builder() .addSkill( BetaSkillParams.builder() .type(ANTHROPIC) .skillId("docx") .version("latest") .build() ) .build() ) .addUserMessage("Write a 2-page report on the benefits of renewable energy") .addTool(BetaCodeExecutionTool20260521.builder().build()) .build() ); ``` ```php PHP $response = $client->beta->messages->create( model: 'claude-opus-5', maxTokens: 16000, betas: ['skills-2025-10-02'], container: [ 'skills' => [ ['type' => 'anthropic', 'skill_id' => 'docx', 'version' => 'latest'], ], ], messages: [ [ 'role' => 'user', 'content' => 'Write a 2-page report on the benefits of renewable energy', ], ], tools: [new BetaCodeExecutionTool20260521()], ); ``` ```ruby Ruby response = client.beta.messages.create( model: "claude-opus-5", max_tokens: 16_000, betas: ["skills-2025-10-02"], container: { skills: [{type: "anthropic", skill_id: "docx", version: "latest"}] }, messages: [ { role: "user", content: "Write a 2-page report on the benefits of renewable energy" } ], tools: [{type: "code_execution_20260521", name: "code_execution"}] ) ``` ### Generate a PDF ```bash cURL curl --fail-with-body -sS https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: skills-2025-10-02" \ -d '{ "model": "claude-opus-5", "max_tokens": 16000, "container": { "skills": [{"type": "anthropic", "skill_id": "pdf", "version": "latest"}] }, "messages": [ {"role": "user", "content": "Generate a PDF invoice template"} ], "tools": [{"type": "code_execution_20260521", "name": "code_execution"}] }' | jq -r '"stop_reason=\(.stop_reason)"' ``` ```bash CLI ant beta:messages create --format json \ --beta skills-2025-10-02 <<'YAML' | jq -r '"stop_reason=\(.stop_reason)"' model: claude-opus-5 max_tokens: 16000 container: skills: - type: anthropic skill_id: pdf version: latest messages: - role: user content: Generate a PDF invoice template tools: - type: code_execution_20260521 name: code_execution YAML ``` ```python Python response = client.beta.messages.create( model="claude-opus-5", max_tokens=16000, betas=["skills-2025-10-02"], container={ "skills": [{"type": "anthropic", "skill_id": "pdf", "version": "latest"}] }, messages=[ { "role": "user", "content": "Generate a PDF invoice template", } ], tools=[{"type": "code_execution_20260521", "name": "code_execution"}], ) ``` ```typescript TypeScript const response = await client.beta.messages.create({ model: "claude-opus-5", max_tokens: 16000, betas: ["skills-2025-10-02"], container: { skills: [{ type: "anthropic", skill_id: "pdf", version: "latest" }] }, messages: [ { role: "user", content: "Generate a PDF invoice template" } ], tools: [{ type: "code_execution_20260521", name: "code_execution" }] }); ``` ```csharp C# var response = await client.Beta.Messages.Create( new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 16000, Betas = ["skills-2025-10-02"], Container = new BetaContainerParams { Skills = [ new BetaSkillParams { Type = BetaSkillParamsType.Anthropic, SkillID = "pdf", Version = "latest", }, ], }, Messages = [ new BetaMessageParam { Role = Role.User, Content = "Generate a PDF invoice template", }, ], Tools = [new BetaCodeExecutionTool20260521()], } ); ``` ```go Go response, err := client.Beta.Messages.New(context.Background(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 16000, Betas: []anthropic.AnthropicBeta{ anthropic.AnthropicBetaSkills2025_10_02, }, Container: anthropic.BetaMessageNewParamsContainerUnion{ OfContainers: &anthropic.BetaContainerParams{ Skills: []anthropic.BetaSkillParams{ { Type: anthropic.BetaSkillParamsTypeAnthropic, SkillID: "pdf", Version: anthropic.String("latest"), }, }, }, }, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Generate a PDF invoice template")), }, Tools: []anthropic.BetaToolUnionParam{ { OfCodeExecutionTool20260521: &anthropic.BetaCodeExecutionTool20260521Param{}, }, }, }) if err != nil { panic(err) } ``` ```java Java BetaMessage response = client.beta().messages().create( MessageCreateParams.builder() .model(CLAUDE_OPUS_5) .maxTokens(16000) .addBeta(AnthropicBeta.SKILLS_2025_10_02) .container( BetaContainerParams.builder() .addSkill( BetaSkillParams.builder() .type(ANTHROPIC) .skillId("pdf") .version("latest") .build() ) .build() ) .addUserMessage("Generate a PDF invoice template") .addTool(BetaCodeExecutionTool20260521.builder().build()) .build() ); ``` ```php PHP $response = $client->beta->messages->create( model: 'claude-opus-5', maxTokens: 16000, betas: ['skills-2025-10-02'], container: [ 'skills' => [ ['type' => 'anthropic', 'skill_id' => 'pdf', 'version' => 'latest'], ], ], messages: [ [ 'role' => 'user', 'content' => 'Generate a PDF invoice template', ], ], tools: [new BetaCodeExecutionTool20260521()], ); ``` ```ruby Ruby response = client.beta.messages.create( model: "claude-opus-5", max_tokens: 16_000, betas: ["skills-2025-10-02"], container: { skills: [{type: "anthropic", skill_id: "pdf", version: "latest"}] }, messages: [ { role: "user", content: "Generate a PDF invoice template" } ], tools: [{type: "code_execution_20260521", name: "code_execution"}] ) ``` ## Next steps Learn how to write effective Skills that Claude can discover and use successfully. Learn how to use Agent Skills to extend Claude's capabilities through the API. Upload your own Skills for specialized tasks. Learn about Skills in Claude Code. Explore example Skills and implementation patterns. --- title: Skill authoring best practices url: https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices description: Learn how to write effective Skills that Claude can discover and use successfully. --- Good Skills are concise, well-structured, and tested with real usage. This guide provides practical authoring decisions to help you write Skills that Claude can discover and use effectively. For conceptual background on how Skills work, see the [Skills overview](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview). ## Core principles ### Concise is key The [context window](https://platform.claude.com/docs/en/build-with-claude/context-windows) is a public good. Your Skill shares the context window with everything else Claude needs to know, including: * The system prompt * Conversation history * Other Skills' metadata * Your actual request Not every token in your Skill has an immediate cost. At startup, only the metadata (name and description) from all Skills is pre-loaded. Claude reads SKILL.md only when the Skill becomes relevant, and reads additional files only as needed. However, being concise in SKILL.md still matters: once Claude loads it, every token competes with conversation history and other context. **Default assumption:** Claude is already very smart Only add context Claude doesn't already have. Challenge each piece of information: * "Does Claude really need this explanation?" * "Can I assume Claude knows this?" * "Does this paragraph justify its token cost?" **Good example: Concise** (approximately 50 tokens): ````markdown ## Extract PDF text Use pdfplumber for text extraction: ```python import pdfplumber with pdfplumber.open("file.pdf") as pdf: text = pdf.pages[0].extract_text() ``` ```` **Bad example: Too verbose** (approximately 150 tokens): ```markdown ## Extract PDF text PDF (Portable Document Format) files are a common file format that contains text, images, and other content. To extract text from a PDF, you'll need to use a library. There are many libraries available for PDF processing, but pdfplumber is recommended because it's easy to use and handles most cases well. First, you'll need to install it using pip. Then you can use the code below... ``` The concise version assumes Claude already has information about PDFs and how libraries work. ### Set appropriate degrees of freedom Match the level of specificity to the task's fragility and variability. **High freedom** (text-based instructions): Use when: * Multiple approaches are valid * Decisions depend on context * Heuristics guide the approach Example: ```markdown ## Code review process 1. Analyze the code structure and organization 2. Check for potential bugs or edge cases 3. Suggest improvements for readability and maintainability 4. Verify adherence to project conventions ``` **Medium freedom** (pseudocode or scripts with parameters): Use when: * A preferred pattern exists * Some variation is acceptable * Configuration affects behavior Example: ````markdown ## Generate report Use this template and customize as needed: ```python def generate_report(data, format="markdown", include_charts=True): # Process data # Generate output in specified format # Optionally include visualizations ``` ```` **Low freedom** (specific scripts, few or no parameters): Use when: * Operations are fragile and error-prone * Consistency is critical * A specific sequence must be followed Example: ````markdown ## Database migration Run exactly this script: ```bash python scripts/migrate.py --verify --backup ``` Do not modify the command or add additional flags. ```` **Analogy:** Think of Claude as a robot exploring a path: * **Narrow bridge with cliffs on both sides:** There's only one safe way forward. Provide specific guardrails and exact instructions (low freedom). Example: database migrations that must run in exact sequence. * **Open field with no hazards:** Many paths lead to success. Give general direction and trust Claude to find the best route (high freedom). Example: code reviews where context determines the best approach. ### Test with all models you plan to use Skills act as additions to models, so effectiveness depends on the underlying model. Test your Skill with all the models you plan to use it with. **Testing considerations by model:** * **Claude Haiku** (fast, economical): Does the Skill provide enough guidance? * **Claude Sonnet** (balanced): Is the Skill clear and efficient? * **Claude Opus** (powerful reasoning): Does the Skill avoid over-explaining? What works perfectly for Opus might need more detail for Haiku. If you plan to use your Skill across multiple models, aim for instructions that work well with all of them. ## Skill structure **YAML Frontmatter:** The SKILL.md frontmatter requires two fields: `name`: * Maximum 64 characters * Must contain only lowercase letters, numbers, and hyphens * Cannot contain XML tags * Cannot contain reserved words: "anthropic", "claude" `description`: * Must be non-empty * Maximum 1,024 characters * Cannot contain XML tags * Should describe what the Skill does and when to use it For complete Skill structure details, see the [Skills overview](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview#skill-structure). ### Naming conventions Use consistent naming patterns to make Skills easier to reference and discuss. Consider using **gerund form** (verb + -ing) for Skill names, as this clearly describes the activity or capability the Skill provides. Remember that the `name` field must use lowercase letters, numbers, and hyphens only. **Good naming examples (gerund form):** * `processing-pdfs` * `analyzing-spreadsheets` * `managing-databases` * `testing-code` * `writing-documentation` **Acceptable alternatives:** * Noun phrases: `pdf-processing`, `spreadsheet-analysis` * Action-oriented: `process-pdfs`, `analyze-spreadsheets` **Avoid:** * Vague names: `helper`, `utils`, `tools` * Overly generic: `documents`, `data`, `files` * Reserved words: `anthropic-helper`, `claude-tools` * Inconsistent patterns within your skill collection Consistent naming makes it easier to: * Reference Skills in documentation and conversations * Understand what a Skill does at a glance * Organize and search through multiple Skills * Maintain a professional, cohesive skill library ### Writing effective descriptions The `description` field enables Skill discovery and should include both what the Skill does and when to use it. **Always write in third person**. The description is injected into the system prompt, and inconsistent point-of-view can cause discovery problems. * **Good:** "Processes Excel files and generates reports" * **Avoid:** "I can help you process Excel files" * **Avoid:** "You can use this to process Excel files" **Be specific and include key terms**. Include both what the Skill does and specific triggers/contexts for when to use it. Each Skill has exactly one description field. The description is critical for skill selection: Claude uses it to choose the right Skill from potentially 100+ available Skills. Your description must provide enough detail for Claude to know when to select this Skill, while the rest of SKILL.md provides the implementation details. Effective examples: **PDF Processing skill:** ```yaml description: Extract text and tables from PDF files, fill forms, merge documents. Use when working with PDF files or when the user mentions PDFs, forms, or document extraction. ``` **Excel Analysis skill:** ```yaml description: Analyze Excel spreadsheets, create pivot tables, generate charts. Use when analyzing Excel files, spreadsheets, tabular data, or .xlsx files. ``` **Git Commit Helper skill:** ```yaml description: Generate descriptive commit messages by analyzing git diffs. Use when the user asks for help writing commit messages or reviewing staged changes. ``` Avoid vague descriptions like these: ```yaml description: Helps with documents ``` ```yaml description: Processes data ``` ```yaml description: Does stuff with files ``` ### Progressive disclosure patterns SKILL.md serves as an overview that points Claude to detailed materials as needed, like a table of contents in an onboarding guide. For an explanation of how progressive disclosure works, see [How Skills work](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview#how-skills-work) in the overview. **Practical guidance:** * Keep SKILL.md body under 500 lines for optimal performance * Split content into separate files when approaching this limit * Use the following patterns to organize instructions, code, and resources effectively #### Visual overview: From simple to complex A basic Skill starts with just a SKILL.md file containing metadata and instructions: ![Simple SKILL.md file showing YAML frontmatter and markdown body](https://platform.claude.com/docs/images/agent-skills-simple-file.png) As your Skill grows, you can bundle additional content that Claude loads only when needed: ![Bundling additional reference files like reference.md and forms.md.](https://platform.claude.com/docs/images/agent-skills-bundling-content.png) The complete Skill directory structure might look like this: * `pdf/` * `SKILL.md`: Main instructions (loaded when triggered) * `FORMS.md`: Form-filling guide (loaded as needed) * `reference.md`: API reference (loaded as needed) * `examples.md`: Usage examples (loaded as needed) * `scripts/` * `analyze_form.py`: Utility script (executed, not loaded) * `fill_form.py`: Form filling script * `validate.py`: Validation script #### Pattern 1: High-level guide with references ````markdown --- name: pdf-processing description: Extracts text and tables from PDF files, fills forms, and merges documents. Use when working with PDF files or when the user mentions PDFs, forms, or document extraction. --- # PDF Processing ## Quick start Extract text with pdfplumber: ```python import pdfplumber with pdfplumber.open("file.pdf") as pdf: text = pdf.pages[0].extract_text() ``` ## Advanced features **Form filling**: See [FORMS.md](FORMS.md) for complete guide **API reference**: See [REFERENCE.md](REFERENCE.md) for all methods **Examples**: See [EXAMPLES.md](EXAMPLES.md) for common patterns ```` Claude loads FORMS.md, REFERENCE.md, or EXAMPLES.md only when needed. #### Pattern 2: Domain-specific organization For Skills with multiple domains, organize content by domain to avoid loading irrelevant context. When a user asks about sales metrics, Claude only needs to read sales-related schemas, not finance or marketing data. This keeps token usage low and context focused. * `bigquery-skill/` * `SKILL.md` (overview and navigation) * `reference/` * `finance.md` (revenue, billing metrics) * `sales.md` (opportunities, pipeline) * `product.md` (API usage, features) * `marketing.md` (campaigns, attribution) ````markdown SKILL.md # BigQuery Data Analysis ## Available datasets **Finance**: Revenue, ARR, billing → See [reference/finance.md](reference/finance.md) **Sales**: Opportunities, pipeline, accounts → See [reference/sales.md](reference/sales.md) **Product**: API usage, features, adoption → See [reference/product.md](reference/product.md) **Marketing**: Campaigns, attribution, email → See [reference/marketing.md](reference/marketing.md) ## Quick search Find specific metrics using grep: ```bash grep -i "revenue" reference/finance.md grep -i "pipeline" reference/sales.md grep -i "api usage" reference/product.md ``` ```` #### Pattern 3: Conditional details Show basic content, link to advanced content: ```markdown # DOCX Processing ## Creating documents Use docx-js for new documents. See [DOCX-JS.md](DOCX-JS.md). ## Editing documents For simple edits, modify the XML directly. **For tracked changes**: See [REDLINING.md](REDLINING.md) **For OOXML details**: See [OOXML.md](OOXML.md) ``` Claude reads REDLINING.md or OOXML.md only when the user needs those features. ### Avoid deeply nested references Claude may partially read files when they're referenced from other referenced files. When encountering nested references, Claude might use commands like `head -100` to preview content rather than reading entire files, resulting in incomplete information. **Keep references one level deep from SKILL.md**. All reference files should link directly from SKILL.md to ensure Claude reads complete files when needed. **Bad example: Too deep**: ```markdown # SKILL.md See [advanced.md](advanced.md)... # advanced.md See [details.md](details.md)... # details.md Here's the actual information... ``` **Good example: One level deep**: ```markdown # SKILL.md **Basic usage**: [instructions in SKILL.md] **Advanced features**: See [advanced.md](advanced.md) **API reference**: See [reference.md](reference.md) **Examples**: See [examples.md](examples.md) ``` ### Structure longer reference files with table of contents For reference files longer than 100 lines, include a table of contents at the top. This ensures Claude can see the full scope of available information even when previewing with partial reads. **Example:** ```markdown # API Reference ## Contents - Authentication and setup - Core methods (create, read, update, delete) - Advanced features (batch operations, webhooks) - Error handling patterns - Code examples ## Authentication and setup ... ## Core methods ... ``` Claude can then read the complete file or jump to specific sections as needed. For details on how this filesystem-based architecture enables progressive disclosure, see the [Runtime environment](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices#runtime-environment) section later in this guide. ## Workflows and feedback loops ### Use workflows for complex tasks Break complex operations into clear, sequential steps. For particularly complex workflows, provide a checklist that Claude can copy into its response and check off as it progresses. **Example 1: Research synthesis workflow** (for Skills without code): ````markdown ## Research synthesis workflow Copy this checklist and track your progress: ``` Research Progress: - [ ] Step 1: Read all source documents - [ ] Step 2: Identify key themes - [ ] Step 3: Cross-reference claims - [ ] Step 4: Create structured summary - [ ] Step 5: Verify citations ``` **Step 1: Read all source documents** Review each document in the `sources/` directory. Note the main arguments and supporting evidence. **Step 2: Identify key themes** Look for patterns across sources. What themes appear repeatedly? Where do sources agree or disagree? **Step 3: Cross-reference claims** For each major claim, verify it appears in the source material. Note which source supports each point. **Step 4: Create structured summary** Organize findings by theme. Include: - Main claim - Supporting evidence from sources - Conflicting viewpoints (if any) **Step 5: Verify citations** Check that every claim references the correct source document. If citations are incomplete, return to Step 3. ```` This example shows how workflows apply to analysis tasks that don't require code. The checklist pattern works for any complex, multistep process. **Example 2: PDF form filling workflow** (for Skills with code): ````markdown ## PDF form filling workflow Copy this checklist and check off items as you complete them: ``` Task Progress: - [ ] Step 1: Analyze the form (run analyze_form.py) - [ ] Step 2: Create field mapping (edit fields.json) - [ ] Step 3: Validate mapping (run validate_fields.py) - [ ] Step 4: Fill the form (run fill_form.py) - [ ] Step 5: Verify output (run verify_output.py) ``` **Step 1: Analyze the form** Run: `python scripts/analyze_form.py input.pdf` This extracts form fields and their locations, saving to `fields.json`. **Step 2: Create field mapping** Edit `fields.json` to add values for each field. **Step 3: Validate mapping** Run: `python scripts/validate_fields.py fields.json` Fix any validation errors before continuing. **Step 4: Fill the form** Run: `python scripts/fill_form.py input.pdf fields.json output.pdf` **Step 5: Verify output** Run: `python scripts/verify_output.py output.pdf` If verification fails, return to Step 2. ```` Clear steps prevent Claude from skipping critical validation. The checklist helps both Claude and you track progress through multistep workflows. ### Implement feedback loops **Common pattern:** Run validator → fix errors → repeat This pattern greatly improves output quality. **Example 1: Style guide compliance** (for Skills without code): ```markdown ## Content review process 1. Draft your content following the guidelines in STYLE_GUIDE.md 2. Review against the checklist: - Check terminology consistency - Verify examples follow the standard format - Confirm all required sections are present 3. If issues found: - Note each issue with specific section reference - Revise the content - Review the checklist again 4. Only proceed when all requirements are met 5. Finalize and save the document ``` This shows the validation loop pattern using reference documents instead of scripts. The "validator" is STYLE\_GUIDE.md, and Claude performs the check by reading and comparing. **Example 2: Document editing process** (for Skills with code): ```markdown ## Document editing process 1. Make your edits to `word/document.xml` 2. **Validate immediately**: `python ooxml/scripts/validate.py unpacked_dir/` 3. If validation fails: - Review the error message carefully - Fix the issues in the XML - Run validation again 4. **Only proceed when validation passes** 5. Rebuild: `python ooxml/scripts/pack.py unpacked_dir/ output.docx` 6. Test the output document ``` The validation loop catches errors early. ## Content guidelines ### Avoid time-sensitive information Don't include information that will become outdated: **Bad example: Time-sensitive** (will become wrong): ```markdown If you're doing this before August 2025, use the old API. After August 2025, use the new API. ``` **Good example** (use "old patterns" section): ```markdown ## Current method Use the v2 API endpoint: `api.example.com/v2/messages` ## Old patterns

Legacy v1 API (deprecated 2025-08) The v1 API used: `api.example.com/v1/messages` This endpoint is no longer supported.
``` The old patterns section provides historical context without cluttering the main content. ### Use consistent terminology Choose one term and use it throughout the Skill: **Good - Consistent:** * Always "API endpoint" * Always "field" * Always "extract" **Bad - Inconsistent:** * Mix "API endpoint", "URL", "API route", "path" * Mix "field", "box", "element", "control" * Mix "extract", "pull", "get", "retrieve" Consistency helps Claude parse and follow instructions. ## Common patterns ### Template pattern Provide templates for output format. Match the level of strictness to your needs. **For strict requirements** (such as API responses or data formats): ````markdown ## Report structure ALWAYS use this exact template structure: ```markdown # [Analysis Title] ## Executive summary [One-paragraph overview of key findings] ## Key findings - Finding 1 with supporting data - Finding 2 with supporting data - Finding 3 with supporting data ## Recommendations 1. Specific actionable recommendation 2. Specific actionable recommendation ``` ```` **For flexible guidance** (when adaptation is useful): ````markdown ## Report structure Here is a sensible default format, but use your best judgment based on the analysis: ```markdown # [Analysis Title] ## Executive summary [Overview] ## Key findings [Adapt sections based on what you discover] ## Recommendations [Tailor to the specific context] ``` Adjust sections as needed for the specific analysis type. ```` ### Examples pattern For Skills where output quality depends on seeing examples, provide input/output pairs just like in regular prompting: ````markdown ## Commit message format Generate commit messages following these examples: **Example 1:** Input: Added user authentication with JWT tokens Output: ``` feat(auth): implement JWT-based authentication Add login endpoint and token validation middleware ``` **Example 2:** Input: Fixed bug where dates displayed incorrectly in reports Output: ``` fix(reports): correct date formatting in timezone conversion Use UTC timestamps consistently across report generation ``` **Example 3:** Input: Updated dependencies and refactored error handling Output: ``` chore: update dependencies and refactor error handling - Upgrade lodash to 4.17.21 - Standardize error response format across endpoints ``` Follow this style: type(scope): brief description, then detailed explanation. ```` Examples convey the desired style and level of detail to Claude more clearly than descriptions alone. ### Conditional workflow pattern Guide Claude through decision points: ```markdown ## Document modification workflow 1. Determine the modification type: **Creating new content?** → Follow "Creation workflow" below **Editing existing content?** → Follow "Editing workflow" below 2. Creation workflow: - Use docx-js library - Build document from scratch - Export to .docx format 3. Editing workflow: - Unpack existing document - Modify XML directly - Validate after each change - Repack when complete ``` If workflows become large or complicated with many steps, consider pushing them into separate files and tell Claude to read the appropriate file based on the task at hand. ## Evaluation and iteration ### Build evaluations first **Create evaluations BEFORE writing extensive documentation.** This ensures your Skill solves real problems rather than documenting imagined ones. **Evaluation-driven development:** 1. **Identify gaps:** Run Claude on representative tasks without a Skill. Document specific failures or missing context 2. **Create evaluations:** Build three scenarios that test these gaps 3. **Establish baseline:** Measure Claude's performance without the Skill 4. **Write minimal instructions:** Create just enough content to address the gaps and pass evaluations 5. **Iterate:** Execute evaluations, compare against baseline, and refine This approach ensures you're solving actual problems rather than anticipating requirements that may never materialize. **Evaluation structure:** ```json { "skills": ["pdf-processing"], "query": "Extract all text from this PDF file and save it to output.txt", "files": ["test-files/document.pdf"], "expected_behavior": [ "Successfully reads the PDF file using an appropriate PDF processing library or command-line tool", "Extracts text content from all pages in the document without missing any pages", "Saves the extracted text to a file named output.txt in a clear, readable format" ] } ``` This example demonstrates a data-driven evaluation with a simple testing rubric. There is not currently a built-in way to run these evaluations. Users can create their own evaluation system. Evaluations are your source of truth for measuring Skill effectiveness. ### Develop Skills iteratively with Claude The most effective Skill development process involves Claude itself. Work with one instance of Claude ("Claude A") to create a Skill that is used by other instances ("Claude B"). Claude A helps you design and refine instructions, while Claude B tests them in real tasks. This works because Claude models understand both how to write effective agent instructions and what information agents need. **Creating a new Skill:** 1. **Complete a task without a Skill:** Work through a problem with Claude A using normal prompting. As you work, you'll naturally provide context, explain preferences, and share procedural knowledge. Notice what information you repeatedly provide. 2. **Identify the reusable pattern:** After completing the task, identify what context you provided that would be useful for similar future tasks. **Example:** If you worked through a BigQuery analysis, you might have provided table names, field definitions, filtering rules (such as "always exclude test accounts"), and common query patterns. 3. **Ask Claude A to create a Skill:** "Create a Skill that captures this BigQuery analysis pattern we just used. Include the table schemas, naming conventions, and the rule about filtering test accounts." Claude models understand the Skill format and structure natively. You don't need special system prompts or a "writing skills" skill to get Claude to help create Skills. Simply ask Claude to create a Skill and it generates properly structured SKILL.md content with appropriate frontmatter and body content. 4. **Review for conciseness:** Check that Claude A hasn't added unnecessary explanations. Ask: "Remove the explanation about what win rate means - Claude already knows that." 5. **Improve information architecture:** Ask Claude A to organize the content more effectively. For example: "Organize this so the table schema is in a separate reference file. We might add more tables later." 6. **Test on similar tasks:** Use the Skill with Claude B (a fresh instance with the Skill loaded) on related use cases. Observe whether Claude B finds the right information, applies rules correctly, and handles the task successfully. 7. **Iterate based on observation:** If Claude B struggles or misses something, return to Claude A with specifics: "When Claude used this Skill, it forgot to filter by date for Q4. Should we add a section about date filtering patterns?" **Iterating on existing Skills:** The same hierarchical pattern continues when improving Skills. You alternate between: * **Working with Claude A** (the expert who helps refine the Skill) * **Testing with Claude B** (the agent using the Skill to perform real work) * **Observing Claude B's behavior** and bringing insights back to Claude A 1. **Use the Skill in real workflows:** Give Claude B (with the Skill loaded) actual tasks, not test scenarios 2. **Observe Claude B's behavior:** Note where it struggles, succeeds, or makes unexpected choices **Example observation:** "When I asked Claude B for a regional sales report, it wrote the query but forgot to filter out test accounts, even though the Skill mentions this rule." 3. **Return to Claude A for improvements:** Share the current SKILL.md and describe what you observed. Ask: "I noticed Claude B forgot to filter test accounts when I asked for a regional report. The Skill mentions filtering, but maybe it's not prominent enough?" 4. **Review Claude A's suggestions:** Claude A might suggest reorganizing to make rules more prominent, using stronger language such as "MUST filter" instead of "always filter," or restructuring the workflow section. 5. **Apply and test changes:** Update the Skill with Claude A's refinements, then test again with Claude B on similar requests 6. **Repeat based on usage:** Continue this observe-refine-test cycle as you encounter new scenarios. Each iteration improves the Skill based on real agent behavior, not assumptions. **Gathering team feedback:** 1. Share Skills with teammates and observe their usage 2. Ask: Does the Skill activate when expected? Are instructions clear? What's missing? 3. Incorporate feedback to address gaps in your own usage patterns **Why this approach works:** Claude A understands agent needs, you provide domain expertise, Claude B reveals gaps through real usage, and iterative refinement improves Skills based on observed behavior rather than assumptions. ### Observe how Claude navigates Skills As you iterate on Skills, pay attention to how Claude actually uses them in practice. Watch for: * **Unexpected exploration paths:** Does Claude read files in an order you didn't anticipate? This might indicate your structure isn't as intuitive as you thought * **Missed connections:** Does Claude fail to follow references to important files? Your links might need to be more explicit or prominent * **Overreliance on certain sections:** If Claude repeatedly reads the same file, consider whether that content should be in the main SKILL.md instead * **Ignored content:** If Claude never accesses a bundled file, it might be unnecessary or poorly signaled in the main instructions Iterate based on these observations rather than assumptions. The 'name' and 'description' in your Skill's metadata are particularly critical. Claude uses these when determining whether to trigger the Skill in response to the current task. Make sure they clearly describe what the Skill does and when it should be used. ## Anti-patterns to avoid ### Avoid Windows-style paths Always use forward slashes in file paths, even on Windows: * ✓ **Good:** `scripts/helper.py`, `reference/guide.md` * ✗ **Avoid:** `scripts\helper.py`, `reference\guide.md` Unix-style paths work across all platforms, while Windows-style paths cause errors on Unix systems. ### Avoid offering too many options Don't present multiple approaches unless necessary: ````markdown **Bad example: Too many choices** (confusing): "You can use pypdf, or pdfplumber, or PyMuPDF, or pdf2image, or..." **Good example: Provide a default** (with escape hatch): "Use pdfplumber for text extraction: ```python import pdfplumber ``` For scanned PDFs requiring OCR, use pdf2image with pytesseract instead." ```` ## Advanced: Skills with executable code The following sections focus on Skills that include executable scripts. If your Skill uses only markdown instructions, skip to [Checklist for effective Skills](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices#checklist-for-effective-skills). ### Solve, don't defer When writing scripts for Skills, handle error conditions rather than deferring to Claude. **Good example: Handle errors explicitly:** ```python def process_file(path): """Process a file, creating it if it doesn't exist.""" try: with open(path) as f: return f.read() except FileNotFoundError: # Create file with default content instead of failing print(f"File {path} not found, creating default") with open(path, "w") as f: f.write("") return "" except PermissionError: # Provide alternative instead of failing print(f"Cannot access {path}, using default") return "" ``` **Bad example: Defer to Claude:** ```python def process_file(path): # Just fail and let Claude figure it out return open(path).read() ``` Configuration parameters should also be justified and documented to avoid "voodoo constants" (Ousterhout's law). If you don't know the right value, how will Claude determine it? **Good example: Self-documenting:** ```python # HTTP requests typically complete within 30 seconds # Longer timeout accounts for slow connections REQUEST_TIMEOUT = 30 # Three retries balances reliability vs speed # Most intermittent failures resolve by the second retry MAX_RETRIES = 3 ``` **Bad example: Magic numbers:** ```python TIMEOUT = 47 # Why 47? RETRIES = 5 # Why 5? ``` ### Provide utility scripts Even if Claude could write a script, pre-made scripts offer advantages: **Benefits of utility scripts:** * More reliable than generated code * Save tokens (no need to include code in context) * Save time (no code generation required) * Ensure consistency across uses ![Bundling executable scripts alongside instruction files](https://platform.claude.com/docs/images/agent-skills-executable-scripts.png) The preceding diagram shows how executable scripts work alongside instruction files. The instruction file (forms.md) references the script, and Claude can execute it without loading its contents into context. **Important distinction:** Make clear in your instructions whether Claude should: * **Execute the script** (most common): "Run `analyze_form.py` to extract fields" * **Read it as reference** (for complex logic): "See `analyze_form.py` for the field extraction algorithm" For most utility scripts, execution is preferred because it's more reliable and efficient. See the following [Runtime environment](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices#runtime-environment) section for details on how script execution works. **Example:** ````markdown ## Utility scripts **analyze_form.py**: Extract all form fields from PDF ```bash python scripts/analyze_form.py input.pdf > fields.json ``` Output format: ```json { "field_name": {"type": "text", "x": 100, "y": 200}, "signature": {"type": "sig", "x": 150, "y": 500} } ``` **validate_boxes.py**: Check for overlapping bounding boxes ```bash python scripts/validate_boxes.py fields.json # Returns: "OK" or lists conflicts ``` **fill_form.py**: Apply field values to PDF ```bash python scripts/fill_form.py input.pdf fields.json output.pdf ``` ```` ### Use visual analysis When inputs can be rendered as images, have Claude analyze them: ````markdown ## Form layout analysis 1. Convert PDF to images: ```bash python scripts/pdf_to_images.py form.pdf ``` 2. Analyze each page image to identify form fields 3. Claude can see field locations and types visually ```` In this example, you'd need to write the `pdf_to_images.py` script. Claude's vision capabilities help analyze layouts and structures. ### Create verifiable intermediate outputs When Claude performs complex, open-ended tasks, it can make mistakes. The "plan-validate-execute" pattern catches errors early by having Claude first create a plan in a structured format, then validate that plan with a script before executing it. **Example:** Imagine asking Claude to update 50 form fields in a PDF based on a spreadsheet. Without validation, Claude might reference non-existent fields, create conflicting values, miss required fields, or apply updates incorrectly. **Solution:** Use the workflow pattern shown earlier (PDF form filling), but add an intermediate `changes.json` file that gets validated before applying changes. The workflow becomes: analyze → **create plan file** → **validate plan** → execute → verify. **Why this pattern works:** * **Catches errors early:** Validation finds problems before changes are applied * **Machine-verifiable:** Scripts provide objective verification * **Reversible planning:** Claude can iterate on the plan without touching originals * **Clear debugging:** Error messages point to specific problems **When to use:** Batch operations, destructive changes, complex validation rules, high-stakes operations. **Implementation tip:** Make validation scripts verbose with specific error messages such as "Field 'signature\_date' not found. Available fields: customer\_name, order\_total, signature\_date\_signed" to help Claude fix issues. ### Package dependencies Skills run in the code execution environment with platform-specific limitations: * **claude.ai:** Can install packages from npm and PyPI and pull from GitHub repositories * **Claude API:** Has no network access and no runtime package installation List required packages in your SKILL.md and verify they're available in the [Code execution tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool) documentation. ### Runtime environment Skills run in a code execution environment with filesystem access, bash commands, and code execution capabilities. For the conceptual explanation of this architecture, see [The Skills architecture](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview#the-skills-architecture) in the overview. **How this affects your authoring:** **How Claude accesses Skills:** 1. **Metadata pre-loaded:** At startup, the name and description from all Skills' YAML frontmatter are loaded into the system prompt 2. **Files read on-demand:** Claude uses bash Read tools to access SKILL.md and other files from the filesystem when needed 3. **Scripts executed efficiently:** Utility scripts can be executed through bash without loading their full contents into context. Only the script's output consumes tokens 4. **No context penalty for large files:** Reference files, data, or documentation don't consume context tokens until actually read * **File paths matter:** Claude navigates your skill directory like a filesystem. Use forward slashes (`reference/guide.md`), not backslashes * **Name files descriptively:** Use names that indicate content: `form_validation_rules.md`, not `doc2.md` * **Organize for discovery:** Structure directories by domain or feature * Good: `reference/finance.md`, `reference/sales.md` * Bad: `docs/file1.md`, `docs/file2.md` * **Bundle comprehensive resources:** Include complete API docs, extensive examples, large datasets; no context penalty until accessed * **Prefer scripts for deterministic operations:** Write `validate_form.py` rather than asking Claude to generate validation code * **Make execution intent clear:** * "Run `analyze_form.py` to extract fields" (execute) * "See `analyze_form.py` for the extraction algorithm" (read as reference) * **Test file access patterns:** Verify Claude can navigate your directory structure by testing with real requests **Example:** * `bigquery-skill/` * `SKILL.md` (overview, points to reference files) * `reference/` * `finance.md` (revenue metrics) * `sales.md` (pipeline data) * `product.md` (usage analytics) When the user asks about revenue, Claude reads SKILL.md, sees the reference to `reference/finance.md`, and calls bash to read just that file. The sales.md and product.md files remain on the filesystem, consuming zero context tokens until needed. This filesystem-based model is what enables progressive disclosure. Claude can navigate and selectively load exactly what each task requires. For complete details on the technical architecture, see [How Skills work](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview#how-skills-work) in the Skills overview. ### MCP tool references If your Skill uses MCP (Model Context Protocol) tools, always use fully qualified tool names to avoid "tool not found" errors. **Format:** `ServerName:tool_name` **Example:** ```markdown Use the BigQuery:bigquery_schema tool to retrieve table schemas. Use the GitHub:create_issue tool to create issues. ``` Where: * `BigQuery` and `GitHub` are MCP server names * `bigquery_schema` and `create_issue` are the tool names within those servers Without the server prefix, Claude may fail to locate the tool, especially when multiple MCP servers are available. ### Avoid assuming tools are installed Don't assume packages are available: ````markdown **Bad example: Assumes installation**: "Use the pdf library to process the file." **Good example: Explicit about dependencies**: "Install required package: `pip install pypdf` Then use it: ```python from pypdf import PdfReader reader = PdfReader("file.pdf") ```" ```` ## Technical notes ### YAML frontmatter requirements The SKILL.md frontmatter requires `name` and `description` fields with specific validation rules: * `name`: Maximum 64 characters, lowercase letters/numbers/hyphens only, no XML tags, no reserved words * `description`: Maximum 1,024 characters, non-empty, no XML tags See the [Skills overview](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview#skill-structure) for complete structure details. ### Token budgets Keep SKILL.md body under 500 lines for optimal performance. If your content exceeds this, split it into separate files using the progressive disclosure patterns described earlier. For architectural details, see the [Skills overview](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview#how-skills-work). ## Checklist for effective Skills Before sharing a Skill, verify: ### Core quality * [ ] Description is specific and includes key terms * [ ] Description includes both what the Skill does and when to use it * [ ] SKILL.md body is under 500 lines * [ ] Additional details are in separate files (if needed) * [ ] No time-sensitive information (or in "old patterns" section) * [ ] Consistent terminology throughout * [ ] Examples are concrete, not abstract * [ ] File references are one level deep * [ ] Progressive disclosure used appropriately * [ ] Workflows have clear steps ### Code and scripts * [ ] Scripts solve problems rather than defer to Claude * [ ] Error handling is explicit and helpful * [ ] No "voodoo constants" (all values justified) * [ ] Required packages listed in instructions and verified as available * [ ] Scripts have clear documentation * [ ] No Windows-style paths (all forward slashes) * [ ] Validation/verification steps for critical operations * [ ] Feedback loops included for quality-critical tasks ### Testing * [ ] At least three evaluations created * [ ] Tested with Haiku, Sonnet, and Opus * [ ] Tested with real usage scenarios * [ ] Team feedback incorporated (if applicable) ## Next steps Create your first Skill Create and manage Skills in Claude Code Upload and use Skills programmatically --- title: Skills for enterprise url: https://platform.claude.com/docs/en/agents-and-tools/agent-skills/enterprise description: Governance, security review, evaluation, and organizational guidance for deploying Agent Skills at enterprise scale. --- This guide is for enterprise admins and architects who need to govern Agent Skills across an organization. It covers how to vet, evaluate, deploy, and manage Skills at scale. For authoring guidance, see [best practices](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices). For architecture details, see the [Skills overview](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview). ## Security review and vetting Deploying Skills in an enterprise requires answering two distinct questions: 1. **Are Skills safe in general?** See the [security considerations](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview#security-considerations) section in the overview for platform-level security details. 2. **How do I vet a specific Skill?** Use the following risk assessment and review checklist. ### Risk tier assessment Evaluate each Skill against these risk indicators before approving deployment: | Risk indicator | What to look for | Concern level | | ------------------------ | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | | Code execution | Scripts in the Skill directory (`*.py`, `*.sh`, `*.js`) | High: scripts run with full environment access | | Instruction manipulation | Directives to ignore safety rules, hide actions from users, or alter Claude's behavior conditionally | High: can bypass security controls | | MCP server references | Instructions referencing MCP tools (`ServerName:tool_name`) | High: extends access beyond the Skill itself | | Network access patterns | URLs, API endpoints, `fetch`, `curl`, or `requests` calls | High: potential data exfiltration vector | | Hardcoded credentials | API keys, tokens, or passwords in Skill files or scripts | High: secrets exposed in Git history and context window | | Filesystem access scope | Paths outside the Skill directory, broad glob patterns, path traversal (`../`) | Medium: may access unintended data | | Tool invocations | Instructions directing Claude to use bash, file operations, or other tools | Medium: review what operations are performed | ### Review checklist Before deploying any Skill from a third party or internal contributor, complete these steps: 1. **Read all Skill directory content.** Review SKILL.md, all referenced markdown files, and any bundled scripts or resources. 2. **Verify script behavior matches stated purpose.** Run scripts in a sandboxed environment and confirm outputs align with the Skill's description. 3. **Check for adversarial instructions.** Look for directives that tell Claude to ignore safety rules, hide actions from users, exfiltrate data through responses, or alter behavior based on specific inputs. 4. **Check for external URL fetches or network calls.** Search scripts and instructions for network access patterns (`http`, `requests.get`, `urllib`, `curl`, `fetch`). 5. **Verify no hardcoded credentials.** Check for API keys, tokens, or passwords in Skill files. Credentials should use environment variables or secure credential stores, never appear in Skill content. 6. **Identify tools and commands the Skill instructs Claude to invoke.** List all bash commands, file operations, and tool references. Consider the combined risk when a Skill uses both file-read and network tools together. 7. **Confirm redirect destinations.** If the Skill references external URLs, verify they point to expected domains. 8. **Verify no data exfiltration patterns.** Look for instructions that read sensitive data and then write, send, or encode it for external transmission, including through Claude's conversational responses. Never deploy Skills from untrusted sources without a full audit. A malicious Skill can direct Claude to execute arbitrary code, access sensitive files, or transmit data externally. Treat Skill installation with the same rigor as installing software on production systems. ## Evaluating Skills before deployment Skills can degrade agent performance if they trigger incorrectly, conflict with other Skills, or provide poor instructions. Require evaluation before any production deployment. ### What to evaluate Establish approval gates for these dimensions before deploying any Skill: | Dimension | What it measures | Example failure | | --------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | | Triggering accuracy | Does the Skill activate for the right queries and stay inactive for unrelated ones? | Skill triggers on every spreadsheet mention, even when the user just wants to discuss data | | Isolation behavior | Does the Skill work correctly on its own? | Skill references files that don't exist in its directory | | Coexistence | Does adding this Skill degrade other Skills? | New Skill's description is too broad, stealing triggers from existing Skills | | Instruction following | Does Claude follow the Skill's instructions accurately? | Claude skips validation steps or uses wrong libraries | | Output quality | Does the Skill produce correct, useful results? | Generated reports have formatting errors or missing data | ### Evaluation requirements Require Skill authors to submit evaluation suites with 3–5 representative queries per Skill, covering cases where the Skill should trigger, should not trigger, and ambiguous edge cases. Require testing across the models your organization uses (Haiku, Sonnet, Opus), because Skill effectiveness varies by model. For detailed guidance on building evaluations, see [evaluation and iteration](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices#evaluation-and-iteration) in best practices. For general evaluation methodology, see [develop test cases](https://platform.claude.com/docs/en/test-and-evaluate/develop-tests). ### Using evaluations for lifecycle decisions Evaluation results signal when to act: * **Declining trigger accuracy:** Update the Skill's description or instructions * **Coexistence conflicts:** Consolidate overlapping Skills or narrow descriptions * **Consistently low output quality:** Rewrite instructions or add validation steps * **Persistent failures across updates:** Deprecate the Skill ## Skill lifecycle management Identify workflows that are repetitive, error-prone, or require specialized knowledge. Map these to organizational roles and determine which are candidates for Skills. Ensure the Skill author follows [best practices](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices). Require a security review using the [review checklist](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/enterprise#review-checklist). Require an evaluation suite before approval. Establish separation of duties: Skill authors should not be their own reviewers. Require evaluations in isolation (Skill alone) and alongside existing Skills (coexistence testing). Verify triggering accuracy, output quality, and absence of regressions across your active Skill set before approving for production. Upload through the Skills API for workspace-wide access. See [Using Skills with the API](https://platform.claude.com/docs/en/build-with-claude/skills-guide) for upload and version management. Document the Skill in your internal registry with purpose, owner, and version. Track usage patterns and collect feedback from users. Rerun evaluations periodically to detect drift or regressions as workflows and models evolve. Usage analytics are not currently available through the Skills API. Implement application-level logging to track which Skills are included in requests. Require the full evaluation suite to pass before promoting new versions. Update Skills when workflows change or evaluation scores decline. Deprecate Skills when evaluations consistently fail or the workflow is retired. ## Organizing Skills at scale ### Recall limits As a general guideline, limit the number of Skills loaded simultaneously to maintain reliable recall accuracy. Each Skill's metadata (name and description) competes for attention in the system prompt. With too many Skills active, Claude may fail to select the right Skill or miss relevant ones entirely. Use your evaluation suite to measure recall accuracy as you add Skills, and stop adding when performance degrades. Note that API requests support a maximum of 8 Skills for each request (see [Using Skills with the API](https://platform.claude.com/docs/en/build-with-claude/skills-guide)). If a role requires more Skills than a single request supports, consider consolidating narrow Skills into broader ones or routing requests to different Skill sets based on task type. ### Start specific, consolidate later Encourage teams to start with narrow, workflow-specific Skills rather than broad, multipurpose ones. As patterns emerge across your organization, consolidate related Skills into role-based bundles. Use evaluations to decide when to consolidate. Merge narrow Skills into a broader one only when the consolidated Skill's evaluations confirm equivalent performance to the individual Skills it replaces. **Example progression:** * Start: `formatting-sales-reports`, `querying-pipeline-data`, `updating-crm-records` * Consolidate: `sales-operations` (when evals confirm equivalent performance) ### Naming and cataloging Use consistent naming conventions across your organization. The [naming conventions](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices#naming-conventions) section in best practices provides formatting guidance. Maintain an internal registry for each Skill with: * **Purpose:** What workflow the Skill supports * **Owner:** Team or individual responsible for maintenance * **Version:** Current deployed version * **Dependencies:** MCP servers, packages, or external services required * **Evaluation status:** Last evaluation date and results ### Role-based bundles Group Skills by organizational role to keep each user's active Skill set focused: * **Sales team:** CRM operations, pipeline reporting, proposal generation * **Engineering:** Code review, deployment workflows, incident response * **Finance:** Report generation, data validation, audit preparation Each role-based bundle should contain only the Skills relevant to that role's daily workflows. ## Distribution and version control ### Source control Store Skill directories in Git for history tracking, code review through pull requests, and rollback capability. Each Skill directory (containing SKILL.md and any bundled files) maps naturally to a Git-tracked folder. ### API-based distribution The Skills API provides workspace-scoped distribution. Skills uploaded through the API are available to all workspace members. See [Using Skills with the API](https://platform.claude.com/docs/en/build-with-claude/skills-guide) for upload, versioning, and management endpoints. ### Versioning strategy * **Production:** Pin Skills to specific versions. Run the full evaluation suite before promoting a new version. Treat every update as a new deployment requiring full security review. * **Development and testing:** Use latest versions to validate changes before production promotion. * **Rollback plan:** Maintain the previous version as a fallback. If a new version fails evaluations in production, revert to the last known-good version immediately. * **Integrity verification:** Compute checksums of reviewed Skills and verify them at deployment time. Use signed commits in your Skill repository to ensure provenance. ### Cross-surface considerations Custom Skills do not sync across surfaces. Skills uploaded to the API are not available on claude.ai or in Claude Code, and vice versa. Each surface requires separate uploads and management. Maintain Skill source files in Git as the single source of truth. If your organization deploys Skills across multiple surfaces, implement your own synchronization process to keep them consistent. For full details, see [cross-surface availability](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview#cross-surface-availability). ## Next steps Architecture and platform details Authoring guidance for Skill creators Upload and manage Skills programmatically --- title: Using Agent Skills with the API url: https://platform.claude.com/docs/en/build-with-claude/skills-guide description: Learn how to use Agent Skills to extend Claude's capabilities through the API. --- Agent Skills extend Claude's capabilities through organized folders of instructions, scripts, and resources. This guide shows you how to use both pre-built and custom Skills with the Claude API. For complete API reference including request/response schemas and all parameters, see: * [Skill Management API Reference](https://platform.claude.com/docs/en/api/beta/skills/list) - CRUD operations for Skills * [Skill Versions API Reference](https://platform.claude.com/docs/en/api/beta/skills/versions/list) - Version management For how zero data retention (ZDR) applies to this feature, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention). ## Quick links Learn how to use Agent Skills to create documents with the Claude API in under 10 minutes. Learn how to write effective Skills that Claude can discover and use successfully. ## Overview For a detailed look at the architecture and real-world applications of Agent Skills, read the engineering blog post: [Equipping agents for the real world with Agent Skills](https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills). Skills integrate with the Messages API through the [code execution tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool). Whether using pre-built Skills managed by Anthropic or custom Skills you've uploaded, the integration shape is identical: both require code execution and use the same `container` structure. ### Using Skills Skills integrate identically in the Messages API regardless of source. You specify Skills in the `container` parameter with a `skill_id`, `type`, and optional `version`, and they run in the code execution environment. You can use Skills from two sources: | Aspect | Anthropic Skills | Custom Skills | | ------------------ | ------------------------------------------ | ------------------------------------------------------------------------------------------------------ | | **Type value** | `anthropic` | `custom` | | **Skill IDs** | Short names: `pptx`, `xlsx`, `docx`, `pdf` | Generated: `skill_01AbCdEfGhIjKlMnOpQrStUv` | | **Version format** | Date-based: `20251013` or `latest` | Epoch timestamp: `1759178010641129` or `latest` | | **Management** | Pre-built and maintained by Anthropic | Upload and manage through the [Skills API](https://platform.claude.com/docs/en/api/beta/skills/create) | | **Availability** | Available to all users | Private to your workspace | Both skill sources are returned by the [List Skills endpoint](https://platform.claude.com/docs/en/api/beta/skills/list) (use the `source` parameter to filter). The integration shape and execution environment are identical. The only difference is where the Skills come from and how they're managed. ### Prerequisites To use Skills, you need: 1. **Claude API key** from the [Claude Console](https://platform.claude.com/settings/keys) 2. **Beta headers:** * `code-execution-2025-08-25` - Enables code execution (required for Skills) * `skills-2025-10-02` - Enables Skills API * `files-api-2025-04-14` - Required only when you use the [Files API](https://platform.claude.com/docs/en/build-with-claude/files) to upload input files or download files a Skill produces 3. **[Code execution tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool)** enabled in your requests Skills require the code execution tool, so use a model from its [model compatibility list](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool#model-compatibility). *** ## Using Skills in Messages ### Container parameter Skills are specified using the `container` parameter in the Messages API. You can include up to 8 Skills for each request. The structure is identical for both Anthropic and custom Skills. Specify the required `type` and `skill_id`, and optionally include `version` to pin to a specific version: ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: code-execution-2025-08-25,skills-2025-10-02" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 4096, "container": { "skills": [ { "type": "anthropic", "skill_id": "pptx", "version": "latest" } ] }, "messages": [{ "role": "user", "content": "Create a presentation about renewable energy" }], "tools": [{ "type": "code_execution_20250825", "name": "code_execution" }] }' ``` ```bash CLI ant beta:messages create \ --beta code-execution-2025-08-25,skills-2025-10-02 <<'YAML' model: claude-opus-5 max_tokens: 4096 container: skills: - type: anthropic skill_id: pptx version: latest messages: - role: user content: Create a presentation about renewable energy tools: - type: code_execution_20250825 name: code_execution YAML ``` ```python Python client = anthropic.Anthropic() response = client.beta.messages.create( model="claude-opus-5", max_tokens=4096, betas=["code-execution-2025-08-25", "skills-2025-10-02"], container={ "skills": [{"type": "anthropic", "skill_id": "pptx", "version": "latest"}] }, messages=[ {"role": "user", "content": "Create a presentation about renewable energy"} ], tools=[{"type": "code_execution_20250825", "name": "code_execution"}], ) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.beta.messages.create({ model: "claude-opus-5", max_tokens: 4096, betas: ["code-execution-2025-08-25", "skills-2025-10-02"], container: { skills: [ { type: "anthropic", skill_id: "pptx", version: "latest" } ] }, messages: [ { role: "user", content: "Create a presentation about renewable energy" } ], tools: [ { type: "code_execution_20250825", name: "code_execution" } ] }); ``` ```csharp C# AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = "claude-opus-5", MaxTokens = 4096, Betas = ["code-execution-2025-08-25", "skills-2025-10-02"], Container = new BetaContainerParams { Skills = [ new BetaSkillParams { Type = BetaSkillParamsType.Anthropic, SkillID = "pptx", Version = "latest", }, ], }, Messages = [new() { Role = Role.User, Content = "Create a presentation about renewable energy" }], Tools = [new BetaCodeExecutionTool20250825()], }; var message = await client.Beta.Messages.Create(parameters); Console.WriteLine(message); ``` ```go Go client := anthropic.NewClient() response, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{ Model: "claude-opus-5", MaxTokens: 4096, Betas: []anthropic.AnthropicBeta{ "code-execution-2025-08-25", anthropic.AnthropicBetaSkills2025_10_02, }, Container: anthropic.BetaMessageNewParamsContainerUnion{ OfContainers: &anthropic.BetaContainerParams{ Skills: []anthropic.BetaSkillParams{ { Type: anthropic.BetaSkillParamsTypeAnthropic, SkillID: "pptx", Version: anthropic.String("latest"), }, }, }, }, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Create a presentation about renewable energy")), }, Tools: []anthropic.BetaToolUnionParam{ {OfCodeExecutionTool20250825: &anthropic.BetaCodeExecutionTool20250825Param{}}, }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java import com.anthropic.models.beta.messages.BetaContainerParams; import com.anthropic.models.beta.messages.BetaSkillParams; import com.anthropic.models.beta.messages.BetaCodeExecutionTool20250825; // ... void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(4096L) .addBeta("code-execution-2025-08-25") .addBeta("skills-2025-10-02") .container(BetaContainerParams.builder() .addSkill(BetaSkillParams.builder() .type(BetaSkillParams.Type.ANTHROPIC) .skillId("pptx") .version("latest") .build()) .build()) .addUserMessage("Create a presentation about renewable energy") .addTool(BetaCodeExecutionTool20250825.builder().build()) .build(); BetaMessage response = client.beta().messages().create(params); System.out.println(response); } ``` ```php PHP $client = new Client(); $message = $client->beta->messages->create( maxTokens: 4096, messages: [ ['role' => 'user', 'content' => 'Create a presentation about renewable energy'] ], model: 'claude-opus-5', betas: ['code-execution-2025-08-25', 'skills-2025-10-02'], container: [ 'skills' => [ [ 'type' => 'anthropic', 'skill_id' => 'pptx', 'version' => 'latest' ] ] ], tools: [ ['type' => 'code_execution_20250825', 'name' => 'code_execution'] ] ); echo $message; ``` ```ruby Ruby client = Anthropic::Client.new message = client.beta.messages.create( model: "claude-opus-5", max_tokens: 4096, betas: ["code-execution-2025-08-25", "skills-2025-10-02"], container: { skills: [ { type: "anthropic", skill_id: "pptx", version: "latest" } ] }, messages: [ { role: "user", content: "Create a presentation about renewable energy" } ], tools: [ { type: "code_execution_20250825", name: "code_execution" } ] ) puts message ``` ### Downloading generated files When Skills create documents (Excel, PowerPoint, PDF, Word), they return `file_id` attributes in the response. You must use the Files API to download these files. **How it works:** 1. Skills create files during code execution. 2. The response includes a `file_id` for each created file, inside code-execution tool result blocks (see [Response format](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool#response-format)). 3. Use the Files API to download the actual file content. 4. Save locally or process as needed. To provide input files for Skills to work on, [upload them with the Files API](https://platform.claude.com/docs/en/build-with-claude/files#uploading-a-file) and reference them in your request with a [container upload block](https://platform.claude.com/docs/en/build-with-claude/files#container-upload-blocks). **Example: creating and downloading an Excel file** ```bash cURL # Step 1: Use a Skill to create a file RESPONSE=$(curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: code-execution-2025-08-25,skills-2025-10-02" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 4096, "container": { "skills": [ {"type": "anthropic", "skill_id": "xlsx", "version": "latest"} ] }, "messages": [{ "role": "user", "content": "Create an Excel file with a simple budget spreadsheet" }], "tools": [{ "type": "code_execution_20250825", "name": "code_execution" }] }') # Step 2: Extract file_id from response (using jq) FILE_ID=$(echo "$RESPONSE" | jq -r '.content[] | select(.type=="bash_code_execution_tool_result") | .content | select(.type=="bash_code_execution_result") | .content[] | select(.file_id) | .file_id') # Step 3: Get filename from metadata FILENAME=$(curl "https://api.anthropic.com/v1/files/$FILE_ID" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: files-api-2025-04-14" | jq -r '.filename') # Step 4: Download the file using Files API curl "https://api.anthropic.com/v1/files/$FILE_ID/content" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: files-api-2025-04-14" \ --output "$FILENAME" echo "Downloaded: $FILENAME" ``` ```bash CLI # Step 1: Use the xlsx Skill to create a file # Step 2: Extract file_id from the response with --transform (GJSON path) FILE_ID=$(ant beta:messages create \ --beta code-execution-2025-08-25,skills-2025-10-02 \ --transform 'content.#.content.content.#.file_id|@flatten|0' \ --raw-output <<'YAML' model: claude-opus-5 max_tokens: 4096 container: skills: - type: anthropic skill_id: xlsx version: latest messages: - role: user content: Create an Excel file with a simple budget spreadsheet tools: - type: code_execution_20250825 name: code_execution YAML ) # Step 3: Get the filename from file metadata FILENAME=$(ant beta:files retrieve-metadata \ --file-id "$FILE_ID" \ --transform filename \ --raw-output) # Step 4: Download the file using Files API ant beta:files download \ --file-id "$FILE_ID" \ --output "$FILENAME" > /dev/null printf 'Downloaded: %s\n' "$FILENAME" ``` ```python Python client = anthropic.Anthropic() # Step 1: Use a Skill to create a file response = client.beta.messages.create( model="claude-opus-5", max_tokens=4096, betas=["code-execution-2025-08-25", "skills-2025-10-02"], container={ "skills": [{"type": "anthropic", "skill_id": "xlsx", "version": "latest"}] }, messages=[ { "role": "user", "content": "Create an Excel file with a simple budget spreadsheet", } ], tools=[{"type": "code_execution_20250825", "name": "code_execution"}], ) # Step 2: Extract file IDs from the response def extract_file_ids(response): file_ids = [] for item in response.content: if item.type == "bash_code_execution_tool_result": content_item = item.content if content_item.type == "bash_code_execution_result": # each content item is a bash_code_execution_output block carrying a file_id for file in content_item.content: file_ids.append(file.file_id) return file_ids # Step 3: Download the file using Files API for file_id in extract_file_ids(response): file_metadata = client.beta.files.retrieve_metadata(file_id=file_id) file_content = client.beta.files.download(file_id=file_id) # Step 4: Save to disk file_content.write_to_file(file_metadata.filename) print(f"Downloaded: {file_metadata.filename}") ``` ```typescript TypeScript import { writeFile } from "node:fs/promises"; const client = new Anthropic(); // Step 1: Use a Skill to create a file const response = await client.beta.messages.create({ model: "claude-opus-5", max_tokens: 4096, betas: ["code-execution-2025-08-25", "skills-2025-10-02"], container: { skills: [{ type: "anthropic", skill_id: "xlsx", version: "latest" }] }, messages: [ { role: "user", content: "Create an Excel file with a simple budget spreadsheet" } ], tools: [{ type: "code_execution_20250825", name: "code_execution" }] }); // Step 2: Extract file IDs from the response const fileIds: string[] = []; for (const block of response.content) { if ( block.type === "bash_code_execution_tool_result" && block.content.type === "bash_code_execution_result" ) { for (const outputBlock of block.content.content) { fileIds.push(outputBlock.file_id); } } } // Step 3: Download each file and save to disk for (const fileId of fileIds) { const fileMetadata = await client.beta.files.retrieveMetadata(fileId); const fileResponse = await client.beta.files.download(fileId); await writeFile(fileMetadata.filename, Buffer.from(await fileResponse.arrayBuffer())); console.log(`Downloaded: ${fileMetadata.filename}`); } ``` ```csharp C# AnthropicClient client = new(); // Step 1: Use a Skill to create a file var parameters = new MessageCreateParams { Model = "claude-opus-5", MaxTokens = 4096, Betas = ["code-execution-2025-08-25", "skills-2025-10-02"], Container = new BetaContainerParams { Skills = [ new BetaSkillParams { Type = BetaSkillParamsType.Anthropic, SkillID = "xlsx", Version = "latest", }, ], }, Messages = [new() { Role = Role.User, Content = "Create an Excel file with a simple budget spreadsheet" }], Tools = [new BetaCodeExecutionTool20250825()], }; var response = await client.Beta.Messages.Create(parameters); // Step 2: Extract file IDs from the response List fileIds = []; foreach (var block in response.Content) { if (block.TryPickBashCodeExecutionToolResult(out var toolResult) && toolResult.Content.TryPickBetaBashCodeExecutionResultBlock(out var result)) { foreach (var output in result.Content) { fileIds.Add(output.FileID); } } } // Step 3: Download each file and save to disk foreach (var fileId in fileIds) { var fileMetadata = await client.Beta.Files.RetrieveMetadata(fileId); using var download = await client.Beta.Files.Download(fileId); using var downloadStream = await download.ReadAsStream(); using var outputFile = File.Create(fileMetadata.Filename); await downloadStream.CopyToAsync(outputFile); Console.WriteLine($"Downloaded: {fileMetadata.Filename}"); } ``` ```go Go func main() { client := anthropic.NewClient() // Step 1: Use a Skill to create a file response, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{ Model: "claude-opus-5", MaxTokens: 4096, Betas: []anthropic.AnthropicBeta{"code-execution-2025-08-25", anthropic.AnthropicBetaSkills2025_10_02}, Container: anthropic.BetaMessageNewParamsContainerUnion{ OfContainers: &anthropic.BetaContainerParams{ Skills: []anthropic.BetaSkillParams{ { Type: anthropic.BetaSkillParamsTypeAnthropic, SkillID: "xlsx", Version: anthropic.String("latest"), }, }, }, }, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Create an Excel file with a simple budget spreadsheet")), }, Tools: []anthropic.BetaToolUnionParam{ {OfCodeExecutionTool20250825: &anthropic.BetaCodeExecutionTool20250825Param{}}, }, }) if err != nil { log.Fatal(err) } // Step 2: Extract file IDs from the response fileIDs := extractFileIDs(response) // Step 3: Download the file using Files API for _, fileID := range fileIDs { fileMetadata, err := client.Beta.Files.GetMetadata(context.TODO(), fileID, anthropic.BetaFileGetMetadataParams{}) if err != nil { log.Fatal(err) } fileContent, err := client.Beta.Files.Download(context.TODO(), fileID, anthropic.BetaFileDownloadParams{}) if err != nil { log.Fatal(err) } // Step 4: Save to disk out, err := os.Create(fileMetadata.Filename) if err != nil { log.Fatal(err) } if _, err := io.Copy(out, fileContent.Body); err != nil { log.Fatal(err) } out.Close() fileContent.Body.Close() fmt.Printf("Downloaded: %s\n", fileMetadata.Filename) } } func extractFileIDs(response *anthropic.BetaMessage) []string { var fileIDs []string for _, item := range response.Content { switch v := item.AsAny().(type) { case anthropic.BetaBashCodeExecutionToolResultBlock: if v.Content.Type == "bash_code_execution_result" { for _, output := range v.Content.Content { fileIDs = append(fileIDs, output.FileID) } } } } return fileIDs } ``` ```java Java import com.anthropic.models.beta.messages.BetaContainerParams; import com.anthropic.models.beta.messages.BetaSkillParams; import com.anthropic.models.beta.messages.BetaCodeExecutionTool20250825; import com.anthropic.models.beta.messages.BetaContentBlock; import com.anthropic.models.beta.files.FileMetadata; import com.anthropic.core.http.HttpResponse; // ... void main() throws Exception { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); // Step 1: Use a Skill to create a file MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(4096L) .addBeta("code-execution-2025-08-25") .addBeta("skills-2025-10-02") .container(BetaContainerParams.builder() .addSkill(BetaSkillParams.builder() .type(BetaSkillParams.Type.ANTHROPIC) .skillId("xlsx") .version("latest") .build()) .build()) .addUserMessage("Create an Excel file with a simple budget spreadsheet") .addTool(BetaCodeExecutionTool20250825.builder().build()) .build(); BetaMessage response = client.beta().messages().create(params); // Step 2: Extract file IDs from the response List fileIds = new ArrayList<>(); for (BetaContentBlock block : response.content()) { if (block.isBashCodeExecutionToolResult()) { var content = block.asBashCodeExecutionToolResult().content(); if (content.isBetaBashCodeExecutionResultBlock()) { for (var outputBlock : content.asBetaBashCodeExecutionResultBlock().content()) { fileIds.add(outputBlock.fileId()); } } } } // Step 3: Download the file using Files API for (String fileId : fileIds) { FileMetadata fileMetadata = client.beta().files().retrieveMetadata(fileId); HttpResponse fileContent = client.beta().files().download(fileId); // Step 4: Save to disk try (InputStream is = fileContent.body(); FileOutputStream fos = new FileOutputStream(fileMetadata.filename())) { is.transferTo(fos); } System.out.println("Downloaded: " + fileMetadata.filename()); } } ``` ```php PHP $client = new Client(); // Step 1: Use a Skill to create a file $response = $client->beta->messages->create( maxTokens: 4096, messages: [ ['role' => 'user', 'content' => 'Create an Excel file with a simple budget spreadsheet'] ], model: 'claude-opus-5', betas: ['code-execution-2025-08-25', 'skills-2025-10-02'], container: [ 'skills' => [ ['type' => 'anthropic', 'skill_id' => 'xlsx', 'version' => 'latest'] ] ], tools: [ ['type' => 'code_execution_20250825', 'name' => 'code_execution'] ] ); // Step 2: Extract file IDs from the response function extractFileIds($response) { $fileIds = []; foreach ($response->content as $item) { if ($item->type === 'bash_code_execution_tool_result') { $contentItem = $item->content; if ($contentItem->type === 'bash_code_execution_result') { foreach ($contentItem->content as $file) { $fileIds[] = $file->fileID; } } } } return $fileIds; } // Step 3: Download the file using Files API foreach (extractFileIds($response) as $fileId) { $fileMetadata = $client->beta->files->retrieveMetadata($fileId); $fileContent = $client->beta->files->download($fileId); // Step 4: Save to disk file_put_contents($fileMetadata->filename, $fileContent); echo "Downloaded: {$fileMetadata->filename}\n"; } ``` ```ruby Ruby client = Anthropic::Client.new # Step 1: Use a Skill to create a file response = client.beta.messages.create( model: "claude-opus-5", max_tokens: 4096, betas: ["code-execution-2025-08-25", "skills-2025-10-02"], container: { skills: [{ type: "anthropic", skill_id: "xlsx", version: "latest" }] }, messages: [ { role: "user", content: "Create an Excel file with a simple budget spreadsheet" } ], tools: [{ type: "code_execution_20250825", name: "code_execution" }] ) # Step 2: Extract file IDs from the response def extract_file_ids(response) file_ids = [] response.content.each do |item| if item.type == :bash_code_execution_tool_result content_item = item.content if content_item.type == :bash_code_execution_result content_item.content.each do |file| file_ids << file.file_id end end end end file_ids end # Step 3: Download the file using Files API extract_file_ids(response).each do |file_id| file_metadata = client.beta.files.retrieve_metadata(file_id) file_content = client.beta.files.download(file_id) # Step 4: Save to disk File.binwrite(file_metadata.filename, file_content.read) puts "Downloaded: #{file_metadata.filename}" end ``` **Additional Files API operations:** ```bash cURL # Get file metadata curl "https://api.anthropic.com/v1/files/$FILE_ID" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: files-api-2025-04-14" # List all files curl "https://api.anthropic.com/v1/files" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: files-api-2025-04-14" # Delete a file curl -X DELETE "https://api.anthropic.com/v1/files/$FILE_ID" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: files-api-2025-04-14" ``` ```bash CLI # Get file metadata ant beta:files retrieve-metadata \ --file-id "$FILE_ID" \ --transform '{filename,size_bytes}' \ --format yaml # List all files ant beta:files list \ --transform '{filename,created_at}' \ --format yaml # Delete a file ant beta:files delete --file-id "$FILE_ID" >/dev/null ``` ```python Python client = anthropic.Anthropic() file_id = "file_011CNha8iCJcU1wXNR6q4V8w" # Get file metadata file_info = client.beta.files.retrieve_metadata(file_id=file_id) print(f"Filename: {file_info.filename}, Size: {file_info.size_bytes} bytes") # List all files for file in client.beta.files.list(): print(f"{file.filename} - {file.created_at}") # Delete a file client.beta.files.delete(file_id=file_id) ``` ```typescript TypeScript const client = new Anthropic(); const fileId = "file_011CNha8iCJcU1wXNR6q4V8w"; // Get file metadata const fileInfo = await client.beta.files.retrieveMetadata(fileId); console.log(`Filename: ${fileInfo.filename}, Size: ${fileInfo.size_bytes} bytes`); // List all files for await (const file of client.beta.files.list()) { console.log(`${file.filename} - ${file.created_at}`); } // Delete a file await client.beta.files.delete(fileId); ``` ```csharp C# AnthropicClient client = new(); var fileId = "file_011CNha8iCJcU1wXNR6q4V8w"; // Get file metadata var fileInfo = await client.Beta.Files.RetrieveMetadata(fileId); Console.WriteLine($"Filename: {fileInfo.Filename}, Size: {fileInfo.SizeBytes} bytes"); // List files await foreach (var file in (await client.Beta.Files.List()).Paginate()) { Console.WriteLine($"{file.Filename} - {file.CreatedAt}"); } // Delete the file await client.Beta.Files.Delete(fileId); ``` ```go Go client := anthropic.NewClient() fileID := "file_011CNha8iCJcU1wXNR6q4V8w" // Get file metadata fileInfo, err := client.Beta.Files.GetMetadata(context.TODO(), fileID, anthropic.BetaFileGetMetadataParams{}) if err != nil { log.Fatal(err) } fmt.Printf("Filename: %s, Size: %d bytes\n", fileInfo.Filename, fileInfo.SizeBytes) // List all files files := client.Beta.Files.ListAutoPaging(context.TODO(), anthropic.BetaFileListParams{}) for files.Next() { file := files.Current() fmt.Printf("%s - %s\n", file.Filename, file.CreatedAt) } if files.Err() != nil { log.Fatal(files.Err()) } // Delete a file _, err = client.Beta.Files.Delete(context.TODO(), fileID, anthropic.BetaFileDeleteParams{}) if err != nil { log.Fatal(err) } ``` ```java Java import com.anthropic.models.beta.files.FileMetadata; import com.anthropic.models.beta.files.FileListPage; // ... void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); String fileId = "file_011CNha8iCJcU1wXNR6q4V8w"; // Get file metadata FileMetadata fileInfo = client.beta().files().retrieveMetadata(fileId); System.out.println("Filename: " + fileInfo.filename() + ", Size: " + fileInfo.sizeBytes() + " bytes"); // List files (first page) FileListPage files = client.beta().files().list(); for (var file : files.data()) { System.out.println(file.filename() + " - " + file.createdAt()); } // Delete a file client.beta().files().delete(fileId); } ``` ```php PHP $client = new Client(); $fileId = 'file_011CNha8iCJcU1wXNR6q4V8w'; // Get file metadata $fileInfo = $client->beta->files->retrieveMetadata($fileId); echo "Filename: {$fileInfo->filename}, Size: {$fileInfo->sizeBytes} bytes\n"; // List files (first page) $files = $client->beta->files->list(); foreach ($files->data as $file) { echo "{$file->filename} - {$file->createdAt->format(DATE_ATOM)}\n"; } // Delete a file $client->beta->files->delete($fileId); ``` ```ruby Ruby client = Anthropic::Client.new file_id = "file_011CNha8iCJcU1wXNR6q4V8w" # Get file metadata file_info = client.beta.files.retrieve_metadata(file_id) puts "Filename: #{file_info.filename}, Size: #{file_info.size_bytes} bytes" # List all files client.beta.files.list.auto_paging_each do |file| puts "#{file.filename} - #{file.created_at}" end # Delete a file client.beta.files.delete(file_id) ``` For complete details, see [Files API](https://platform.claude.com/docs/en/build-with-claude/files). ### Multi-turn conversations The response's `container` object carries the container's `id` and `expires_at` timestamp (see [Container reuse](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool#container-reuse) for lifetime details). Reuse the same container across multiple messages by specifying the container ID: ```bash cURL # Multi-turn container reuse doesn't translate well to a one-off shell # command; one of the SDK options would be a better fit. Capture # container.id from the first response, then pass it in the next request as # "container": {"id": "...", "skills": [...]} with the conversation history. ``` ```bash CLI # First request creates container CONTAINER_ID=$(ant beta:messages create \ --beta code-execution-2025-08-25,skills-2025-10-02 \ --transform container.id \ --raw-output <<'YAML' model: claude-opus-5 max_tokens: 4096 container: skills: - {type: anthropic, skill_id: xlsx, version: latest} messages: - role: user content: Create a sample sales dataset and analyze it tools: - {type: code_execution_20250825, name: code_execution} YAML ) # Continue conversation with same container ant beta:messages create \ --beta code-execution-2025-08-25,skills-2025-10-02 < block.type === "text") .map((block) => block.text) .join("\n") }, { role: "user", content: "What was the total revenue?" } ]; const response2 = await client.beta.messages.create({ model: "claude-opus-5", max_tokens: 4096, betas: ["code-execution-2025-08-25", "skills-2025-10-02"], container: { id: response1.container!.id, // Reuse container skills: [{ type: "anthropic", skill_id: "xlsx", version: "latest" }] }, messages, tools: [{ type: "code_execution_20250825", name: "code_execution" }] }); ``` ```csharp C# AnthropicClient client = new(); // First request with a Skill var parameters1 = new MessageCreateParams { Model = "claude-opus-5", MaxTokens = 4096, Betas = ["code-execution-2025-08-25", "skills-2025-10-02"], Container = new BetaContainerParams { Skills = [ new BetaSkillParams { Type = BetaSkillParamsType.Anthropic, SkillID = "xlsx", Version = "latest", }, ], }, Messages = [new() { Role = Role.User, Content = "Create a sample sales dataset and analyze it" }], Tools = [new BetaCodeExecutionTool20250825()], }; var response1 = await client.Beta.Messages.Create(parameters1); // Continue the conversation in the same container // Carry the assistant's text forward; container.id carries the execution state var assistantText = string.Join( "\n", response1.Content.Select(block => block.TryPickText(out var text) ? text.Text : null).Where(text => text is not null) ); var parameters2 = new MessageCreateParams { Model = "claude-opus-5", MaxTokens = 4096, Betas = ["code-execution-2025-08-25", "skills-2025-10-02"], Container = new BetaContainerParams { ID = response1.Container!.ID, Skills = [ new BetaSkillParams { Type = BetaSkillParamsType.Anthropic, SkillID = "xlsx", Version = "latest", }, ], }, Messages = [ new() { Role = Role.User, Content = "Create a sample sales dataset and analyze it" }, new() { Role = Role.Assistant, Content = assistantText }, new() { Role = Role.User, Content = "What was the total revenue?" }, ], Tools = [new BetaCodeExecutionTool20250825()], }; var response2 = await client.Beta.Messages.Create(parameters2); Console.WriteLine(response2); ``` ```go Go client := anthropic.NewClient() response1, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{ Model: "claude-opus-5", MaxTokens: 4096, Betas: []anthropic.AnthropicBeta{"code-execution-2025-08-25", anthropic.AnthropicBetaSkills2025_10_02}, Container: anthropic.BetaMessageNewParamsContainerUnion{ OfContainers: &anthropic.BetaContainerParams{ Skills: []anthropic.BetaSkillParams{ { Type: anthropic.BetaSkillParamsTypeAnthropic, SkillID: "xlsx", Version: anthropic.String("latest"), }, }, }, }, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Create a sample sales dataset and analyze it")), }, Tools: []anthropic.BetaToolUnionParam{ {OfCodeExecutionTool20250825: &anthropic.BetaCodeExecutionTool20250825Param{}}, }, }) if err != nil { log.Fatal(err) } // Carry the assistant's text forward; container.id carries the execution state var textParts []string for _, block := range response1.Content { if block.Type == "text" { textParts = append(textParts, block.Text) } } assistantText := strings.Join(textParts, "\n") response2, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{ Model: "claude-opus-5", MaxTokens: 4096, Betas: []anthropic.AnthropicBeta{"code-execution-2025-08-25", anthropic.AnthropicBetaSkills2025_10_02}, Container: anthropic.BetaMessageNewParamsContainerUnion{ OfContainers: &anthropic.BetaContainerParams{ ID: anthropic.String(response1.Container.ID), // Reuse container Skills: []anthropic.BetaSkillParams{ { Type: anthropic.BetaSkillParamsTypeAnthropic, SkillID: "xlsx", Version: anthropic.String("latest"), }, }, }, }, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Create a sample sales dataset and analyze it")), { Role: anthropic.BetaMessageParamRoleAssistant, Content: []anthropic.BetaContentBlockParamUnion{anthropic.NewBetaTextBlock(assistantText)}, }, anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("What was the total revenue?")), }, Tools: []anthropic.BetaToolUnionParam{ {OfCodeExecutionTool20250825: &anthropic.BetaCodeExecutionTool20250825Param{}}, }, }) if err != nil { log.Fatal(err) } fmt.Println(response2) ``` ```java Java import com.anthropic.models.beta.messages.BetaContainerParams; import com.anthropic.models.beta.messages.BetaSkillParams; import com.anthropic.models.beta.messages.BetaCodeExecutionTool20250825; import com.anthropic.models.beta.messages.BetaContentBlock; // ... void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params1 = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(4096L) .addBeta("code-execution-2025-08-25") .addBeta("skills-2025-10-02") .container(BetaContainerParams.builder() .addSkill(BetaSkillParams.builder() .type(BetaSkillParams.Type.ANTHROPIC) .skillId("xlsx") .version("latest") .build()) .build()) .addUserMessage("Create a sample sales dataset and analyze it") .addTool(BetaCodeExecutionTool20250825.builder().build()) .build(); BetaMessage response1 = client.beta().messages().create(params1); MessageCreateParams params2 = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(4096L) .addBeta("code-execution-2025-08-25") .addBeta("skills-2025-10-02") .container(BetaContainerParams.builder() .id(response1.container().get().id()) .addSkill(BetaSkillParams.builder() .type(BetaSkillParams.Type.ANTHROPIC) .skillId("xlsx") .version("latest") .build()) .build()) .addUserMessage("Create a sample sales dataset and analyze it") // Carry the assistant's text forward; container.id carries the execution state .addAssistantMessage(response1.content().stream() .filter(BetaContentBlock::isText) .map(block -> block.asText().text()) .collect(Collectors.joining("\n"))) .addUserMessage("What was the total revenue?") .addTool(BetaCodeExecutionTool20250825.builder().build()) .build(); BetaMessage response2 = client.beta().messages().create(params2); System.out.println(response2); } ``` ```php PHP $client = new Client(); $response1 = $client->beta->messages->create( maxTokens: 4096, messages: [ ['role' => 'user', 'content' => 'Create a sample sales dataset and analyze it'] ], model: 'claude-opus-5', betas: ['code-execution-2025-08-25', 'skills-2025-10-02'], container: [ 'skills' => [ ['type' => 'anthropic', 'skill_id' => 'xlsx', 'version' => 'latest'] ] ], tools: [ ['type' => 'code_execution_20250825', 'name' => 'code_execution'] ] ); $messages = [ ['role' => 'user', 'content' => 'Create a sample sales dataset and analyze it'], // Carry the assistant's text forward; container.id carries the execution state ['role' => 'assistant', 'content' => implode("\n", array_map( fn ($block) => $block->text, array_filter($response1->content, fn ($block) => $block->type === 'text'), ))], ['role' => 'user', 'content' => 'What was the total revenue?'] ]; $response2 = $client->beta->messages->create( maxTokens: 4096, messages: $messages, model: 'claude-opus-5', betas: ['code-execution-2025-08-25', 'skills-2025-10-02'], container: [ 'id' => $response1->container->id, 'skills' => [ ['type' => 'anthropic', 'skill_id' => 'xlsx', 'version' => 'latest'] ] ], tools: [ ['type' => 'code_execution_20250825', 'name' => 'code_execution'] ] ); echo $response2; ``` ```ruby Ruby client = Anthropic::Client.new response1 = client.beta.messages.create( model: "claude-opus-5", max_tokens: 4096, betas: ["code-execution-2025-08-25", "skills-2025-10-02"], container: { skills: [{ type: "anthropic", skill_id: "xlsx", version: "latest" }] }, messages: [ { role: "user", content: "Create a sample sales dataset and analyze it" } ], tools: [ { type: "code_execution_20250825", name: "code_execution" } ] ) messages = [ { role: "user", content: "Create a sample sales dataset and analyze it" }, { # Carry the assistant's text forward; container.id carries the execution state role: "assistant", content: response1.content.filter_map { |block| block.text if block.type == :text }.join("\n") }, { role: "user", content: "What was the total revenue?" } ] response2 = client.beta.messages.create( model: "claude-opus-5", max_tokens: 4096, betas: ["code-execution-2025-08-25", "skills-2025-10-02"], container: { id: response1.container.id, skills: [ { type: "anthropic", skill_id: "xlsx", version: "latest" } ] }, messages: messages, tools: [ { type: "code_execution_20250825", name: "code_execution" } ] ) puts response2 ``` ### Long-running operations Skills may perform operations that require multiple turns. Handle `pause_turn` stop reasons: ```bash cURL # Initial request RESPONSE=$(curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: code-execution-2025-08-25,skills-2025-10-02" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 4096, "container": { "skills": [ { "type": "custom", "skill_id": "skill_01AbCdEfGhIjKlMnOpQrStUv", "version": "latest" } ] }, "messages": [{ "role": "user", "content": "Generate and process a large sample dataset" }], "tools": [{ "type": "code_execution_20250825", "name": "code_execution" }] }') # If stop_reason is "pause_turn", continue in the same container, appending # the prior response's content array to messages as the assistant turn. # Repeat this continuation request until stop_reason is no longer "pause_turn". STOP_REASON=$(echo "$RESPONSE" | jq -r '.stop_reason') CONTAINER_ID=$(echo "$RESPONSE" | jq -r '.container.id') RESPONSE=$(curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: code-execution-2025-08-25,skills-2025-10-02" \ -H "content-type: application/json" \ -d "{ \"model\": \"claude-opus-5\", \"max_tokens\": 4096, \"container\": { \"id\": \"$CONTAINER_ID\", \"skills\": [{ \"type\": \"custom\", \"skill_id\": \"skill_01AbCdEfGhIjKlMnOpQrStUv\", \"version\": \"latest\" }] }, \"messages\": [], \"tools\": [{ \"type\": \"code_execution_20250825\", \"name\": \"code_execution\" }] }") ``` ```bash CLI RESP=$(mktemp) # Initial request: capture the full JSON response to a temp file ant beta:messages create \ --beta code-execution-2025-08-25,skills-2025-10-02 \ > "$RESP" <<'YAML' model: claude-opus-5 max_tokens: 4096 container: skills: - type: custom skill_id: skill_01AbCdEfGhIjKlMnOpQrStUv version: latest messages: - role: user content: Generate and process a large sample dataset tools: - type: code_execution_20250825 name: code_execution YAML # If stop_reason is "pause_turn", continue in the same container, # appending the prior response's content array to messages as the # assistant turn. Repeat until stop_reason is no longer "pause_turn". CONTAINER_ID=$(jq -r '.container.id' "$RESP") ant beta:messages create \ --beta code-execution-2025-08-25,skills-2025-10-02 \ > "$RESP" < messages = [ new() { Role = Role.User, Content = "Generate and process a large sample dataset" }, ]; var maxRetries = 10; string? containerId = null; BetaMessage? response = null; for (var i = 0; i < maxRetries; i++) { var parameters = new MessageCreateParams { Model = "claude-opus-5", MaxTokens = 4096, Betas = ["code-execution-2025-08-25", "skills-2025-10-02"], Container = containerId is null ? new BetaContainerParams { Skills = [ new BetaSkillParams { Type = BetaSkillParamsType.Custom, SkillID = "skill_01AbCdEfGhIjKlMnOpQrStUv", Version = "latest", }, ], } : new BetaContainerParams { ID = containerId, Skills = [ new BetaSkillParams { Type = BetaSkillParamsType.Custom, SkillID = "skill_01AbCdEfGhIjKlMnOpQrStUv", Version = "latest", }, ], }, Messages = messages, Tools = [new BetaCodeExecutionTool20250825()], }; response = await client.Beta.Messages.Create(parameters); containerId = response.Container!.ID; if (response.StopReason != BetaStopReason.PauseTurn) { break; } // Append the paused turn's content and continue var assistantContent = JsonSerializer.SerializeToElement( response.Content.Select(block => block.Json).ToArray() ); messages.Add(new() { Role = Role.Assistant, Content = new BetaMessageParamContent(assistantContent) }); } ``` ```go Go client := anthropic.NewClient() messages := []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Generate and process a large sample dataset")), } maxRetries := 10 response, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{ Model: "claude-opus-5", MaxTokens: 4096, Betas: []anthropic.AnthropicBeta{"code-execution-2025-08-25", anthropic.AnthropicBetaSkills2025_10_02}, Container: anthropic.BetaMessageNewParamsContainerUnion{ OfContainers: &anthropic.BetaContainerParams{ Skills: []anthropic.BetaSkillParams{ { Type: anthropic.BetaSkillParamsTypeCustom, SkillID: "skill_01AbCdEfGhIjKlMnOpQrStUv", Version: anthropic.String("latest"), }, }, }, }, Messages: messages, Tools: []anthropic.BetaToolUnionParam{ {OfCodeExecutionTool20250825: &anthropic.BetaCodeExecutionTool20250825Param{}}, }, }) if err != nil { log.Fatal(err) } for i := 0; i < maxRetries; i++ { if response.StopReason != anthropic.BetaStopReasonPauseTurn { break } messages = append(messages, response.ToParam()) response, err = client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{ Model: "claude-opus-5", MaxTokens: 4096, Betas: []anthropic.AnthropicBeta{"code-execution-2025-08-25", anthropic.AnthropicBetaSkills2025_10_02}, Container: anthropic.BetaMessageNewParamsContainerUnion{ OfContainers: &anthropic.BetaContainerParams{ ID: anthropic.String(response.Container.ID), // Reuse container Skills: []anthropic.BetaSkillParams{ { Type: anthropic.BetaSkillParamsTypeCustom, SkillID: "skill_01AbCdEfGhIjKlMnOpQrStUv", Version: anthropic.String("latest"), }, }, }, }, Messages: messages, Tools: []anthropic.BetaToolUnionParam{ {OfCodeExecutionTool20250825: &anthropic.BetaCodeExecutionTool20250825Param{}}, }, }) if err != nil { log.Fatal(err) } } fmt.Println(response) ``` ```java Java import com.anthropic.models.beta.messages.BetaContainerParams; import com.anthropic.models.beta.messages.BetaSkillParams; import com.anthropic.models.beta.messages.BetaCodeExecutionTool20250825; import com.anthropic.models.beta.messages.BetaStopReason; // ... void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); List messages = new ArrayList<>(); messages.add( BetaMessageParam.builder() .role(BetaMessageParam.Role.USER) .content("Generate and process a large sample dataset") .build() ); int maxRetries = 10; BetaMessage response = client.beta().messages().create( MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(4096L) .addBeta("code-execution-2025-08-25") .addBeta("skills-2025-10-02") .container(BetaContainerParams.builder() .addSkill(BetaSkillParams.builder() .type(BetaSkillParams.Type.CUSTOM) .skillId("skill_01AbCdEfGhIjKlMnOpQrStUv") .version("latest") .build()) .build()) .messages(messages) .addTool(BetaCodeExecutionTool20250825.builder().build()) .build()); for (int i = 0; i < maxRetries; i++) { if (!response.stopReason().isPresent() || !response.stopReason().get().equals(BetaStopReason.PAUSE_TURN)) { break; } messages.add(response.toParam()); response = client.beta().messages().create( MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(4096L) .addBeta("code-execution-2025-08-25") .addBeta("skills-2025-10-02") .container(BetaContainerParams.builder() .id(response.container().get().id()) .addSkill(BetaSkillParams.builder() .type(BetaSkillParams.Type.CUSTOM) .skillId("skill_01AbCdEfGhIjKlMnOpQrStUv") .version("latest") .build()) .build()) .messages(messages) .addTool(BetaCodeExecutionTool20250825.builder().build()) .build()); } } ``` ```php PHP $client = new Client(); $messages = [ ['role' => 'user', 'content' => 'Generate and process a large sample dataset'] ]; $maxRetries = 10; $response = $client->beta->messages->create( maxTokens: 4096, messages: $messages, model: 'claude-opus-5', betas: ['code-execution-2025-08-25', 'skills-2025-10-02'], container: [ 'skills' => [ [ 'type' => 'custom', 'skill_id' => 'skill_01AbCdEfGhIjKlMnOpQrStUv', 'version' => 'latest' ] ] ], tools: [['type' => 'code_execution_20250825', 'name' => 'code_execution']] ); for ($i = 0; $i < $maxRetries; $i++) { if ($response->stopReason !== 'pause_turn') { break; } $messages[] = ['role' => 'assistant', 'content' => $response->content]; $response = $client->beta->messages->create( maxTokens: 4096, messages: $messages, model: 'claude-opus-5', betas: ['code-execution-2025-08-25', 'skills-2025-10-02'], container: [ 'id' => $response->container->id, 'skills' => [ [ 'type' => 'custom', 'skill_id' => 'skill_01AbCdEfGhIjKlMnOpQrStUv', 'version' => 'latest' ] ] ], tools: [['type' => 'code_execution_20250825', 'name' => 'code_execution']] ); } ``` ```ruby Ruby client = Anthropic::Client.new messages = [ { role: "user", content: "Generate and process a large sample dataset" } ] max_retries = 10 response = client.beta.messages.create( model: "claude-opus-5", max_tokens: 4096, betas: ["code-execution-2025-08-25", "skills-2025-10-02"], container: { skills: [ { type: "custom", skill_id: "skill_01AbCdEfGhIjKlMnOpQrStUv", version: "latest" } ] }, messages: messages, tools: [{ type: "code_execution_20250825", name: "code_execution" }] ) max_retries.times do break if response.stop_reason != :pause_turn messages << { role: "assistant", content: response.content } response = client.beta.messages.create( model: "claude-opus-5", max_tokens: 4096, betas: ["code-execution-2025-08-25", "skills-2025-10-02"], container: { id: response.container.id, skills: [ { type: "custom", skill_id: "skill_01AbCdEfGhIjKlMnOpQrStUv", version: "latest" } ] }, messages: messages, tools: [{ type: "code_execution_20250825", name: "code_execution" }] ) end ``` The response may include a `pause_turn` stop reason, which indicates that the API paused a long-running Skill operation. You can provide the response back as-is in a subsequent request to let Claude continue its turn, or modify the content if you want to interrupt the conversation and provide additional guidance. ### Using multiple Skills Combine multiple Skills in a single request to handle complex workflows: ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: code-execution-2025-08-25,skills-2025-10-02" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 4096, "container": { "skills": [ { "type": "anthropic", "skill_id": "xlsx", "version": "latest" }, { "type": "anthropic", "skill_id": "pptx", "version": "latest" }, { "type": "custom", "skill_id": "skill_01AbCdEfGhIjKlMnOpQrStUv", "version": "latest" } ] }, "messages": [{ "role": "user", "content": "Analyze sales data and create a presentation" }], "tools": [{ "type": "code_execution_20250825", "name": "code_execution" }] }' ``` ```bash CLI ant beta:messages create \ --beta code-execution-2025-08-25,skills-2025-10-02 <<'YAML' model: claude-opus-5 max_tokens: 4096 container: skills: - type: anthropic skill_id: xlsx version: latest - type: anthropic skill_id: pptx version: latest - type: custom skill_id: skill_01AbCdEfGhIjKlMnOpQrStUv version: latest messages: - role: user content: Analyze sales data and create a presentation tools: - type: code_execution_20250825 name: code_execution YAML ``` ```python Python client = anthropic.Anthropic() response = client.beta.messages.create( model="claude-opus-5", max_tokens=4096, betas=["code-execution-2025-08-25", "skills-2025-10-02"], container={ "skills": [ {"type": "anthropic", "skill_id": "xlsx", "version": "latest"}, {"type": "anthropic", "skill_id": "pptx", "version": "latest"}, { "type": "custom", "skill_id": "skill_01AbCdEfGhIjKlMnOpQrStUv", "version": "latest", }, ] }, messages=[ {"role": "user", "content": "Analyze sales data and create a presentation"} ], tools=[{"type": "code_execution_20250825", "name": "code_execution"}], ) ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.beta.messages.create({ model: "claude-opus-5", max_tokens: 4096, betas: ["code-execution-2025-08-25", "skills-2025-10-02"], container: { skills: [ { type: "anthropic", skill_id: "xlsx", version: "latest" }, { type: "anthropic", skill_id: "pptx", version: "latest" }, { type: "custom", skill_id: "skill_01AbCdEfGhIjKlMnOpQrStUv", version: "latest" } ] }, messages: [ { role: "user", content: "Analyze sales data and create a presentation" } ], tools: [ { type: "code_execution_20250825", name: "code_execution" } ] }); ``` ```csharp C# AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = "claude-opus-5", MaxTokens = 4096, Betas = ["code-execution-2025-08-25", "skills-2025-10-02"], Container = new BetaContainerParams { Skills = [ new BetaSkillParams { Type = BetaSkillParamsType.Anthropic, SkillID = "xlsx", Version = "latest", }, new BetaSkillParams { Type = BetaSkillParamsType.Anthropic, SkillID = "pptx", Version = "latest", }, new BetaSkillParams { Type = BetaSkillParamsType.Custom, SkillID = "skill_01AbCdEfGhIjKlMnOpQrStUv", Version = "latest", }, ], }, Messages = [new() { Role = Role.User, Content = "Analyze sales data and create a presentation" }], Tools = [new BetaCodeExecutionTool20250825()], }; var message = await client.Beta.Messages.Create(parameters); Console.WriteLine(message); ``` ```go Go client := anthropic.NewClient() response, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{ Model: "claude-opus-5", MaxTokens: 4096, Betas: []anthropic.AnthropicBeta{ "code-execution-2025-08-25", anthropic.AnthropicBetaSkills2025_10_02, }, Container: anthropic.BetaMessageNewParamsContainerUnion{ OfContainers: &anthropic.BetaContainerParams{ Skills: []anthropic.BetaSkillParams{ { Type: anthropic.BetaSkillParamsTypeAnthropic, SkillID: "xlsx", Version: anthropic.String("latest"), }, { Type: anthropic.BetaSkillParamsTypeAnthropic, SkillID: "pptx", Version: anthropic.String("latest"), }, { Type: anthropic.BetaSkillParamsTypeCustom, SkillID: "skill_01AbCdEfGhIjKlMnOpQrStUv", Version: anthropic.String("latest"), }, }, }, }, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Analyze sales data and create a presentation")), }, Tools: []anthropic.BetaToolUnionParam{ {OfCodeExecutionTool20250825: &anthropic.BetaCodeExecutionTool20250825Param{}}, }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java import com.anthropic.models.beta.messages.BetaContainerParams; import com.anthropic.models.beta.messages.BetaSkillParams; import com.anthropic.models.beta.messages.BetaCodeExecutionTool20250825; // ... void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(4096L) .addBeta("code-execution-2025-08-25") .addBeta("skills-2025-10-02") .container(BetaContainerParams.builder() .skills(List.of( BetaSkillParams.builder() .type(BetaSkillParams.Type.ANTHROPIC) .skillId("xlsx") .version("latest") .build(), BetaSkillParams.builder() .type(BetaSkillParams.Type.ANTHROPIC) .skillId("pptx") .version("latest") .build(), BetaSkillParams.builder() .type(BetaSkillParams.Type.CUSTOM) .skillId("skill_01AbCdEfGhIjKlMnOpQrStUv") .version("latest") .build() )) .build()) .addUserMessage("Analyze sales data and create a presentation") .addTool(BetaCodeExecutionTool20250825.builder().build()) .build(); BetaMessage response = client.beta().messages().create(params); System.out.println(response); } ``` ```php PHP $client = new Client(); $message = $client->beta->messages->create( maxTokens: 4096, messages: [ ['role' => 'user', 'content' => 'Analyze sales data and create a presentation'] ], model: 'claude-opus-5', betas: ['code-execution-2025-08-25', 'skills-2025-10-02'], container: [ 'skills' => [ [ 'type' => 'anthropic', 'skill_id' => 'xlsx', 'version' => 'latest' ], [ 'type' => 'anthropic', 'skill_id' => 'pptx', 'version' => 'latest' ], [ 'type' => 'custom', 'skill_id' => 'skill_01AbCdEfGhIjKlMnOpQrStUv', 'version' => 'latest' ] ] ], tools: [ ['type' => 'code_execution_20250825', 'name' => 'code_execution'] ] ); echo $message; ``` ```ruby Ruby client = Anthropic::Client.new message = client.beta.messages.create( model: "claude-opus-5", max_tokens: 4096, betas: ["code-execution-2025-08-25", "skills-2025-10-02"], container: { skills: [ { type: "anthropic", skill_id: "xlsx", version: "latest" }, { type: "anthropic", skill_id: "pptx", version: "latest" }, { type: "custom", skill_id: "skill_01AbCdEfGhIjKlMnOpQrStUv", version: "latest" } ] }, messages: [ { role: "user", content: "Analyze sales data and create a presentation" } ], tools: [ { type: "code_execution_20250825", name: "code_execution" } ] ) puts message ``` *** ## Managing custom Skills ### Creating a Skill A Skill bundle is a directory containing a `SKILL.md` file at the top level with `name` and `description` YAML frontmatter, plus any supporting scripts or resources. See [Get started with Agent Skills in the API](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/quickstart) to author one, and the **Requirements** list following the examples for the full constraints. Upload your custom Skill to make it available in your workspace. You can upload a zip archive or individual file objects. The Python SDK also provides a `files_from_dir` helper that accepts a directory path. Files are identified by the filename you attach. Per-file uploads must keep a common top-level directory in their paths (the `;filename=` suffix in the cURL example and the filename arguments in the SDK examples). A zip archive must contain the skill directory as its single top-level entry. For the walkthrough's skill, create one with `zip -r financial_skill.zip financial_skill/` and substitute it for the `example_skill.zip` placeholder in the zip-upload options. ```bash cURL curl -X POST "https://api.anthropic.com/v1/skills" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: skills-2025-10-02" \ -F "files[]=@financial_skill/SKILL.md;filename=financial_skill/SKILL.md" \ -F "files[]=@financial_skill/analyze.py;filename=financial_skill/analyze.py" ``` ```bash CLI ant beta:skills create \ --file example_skill.zip \ --beta skills-2025-10-02 # Per-file upload requires path-qualified filenames, which the CLI # can't currently set. Upload a zip archive instead. ``` ```python Python from anthropic.lib import files_from_dir client = anthropic.Anthropic() # Option 1: Using a zip file skill = client.beta.skills.create( files=[open("example_skill.zip", "rb")], ) # Option 2: Using file tuples (filename, file_content, mime_type) skill = client.beta.skills.create( files=[ ( "financial_skill/SKILL.md", open("financial_skill/SKILL.md", "rb"), "text/markdown", ), ( "financial_skill/analyze.py", open("financial_skill/analyze.py", "rb"), "text/x-python", ), ], ) # Option 3: Using the files_from_dir helper (Python only) skill = client.beta.skills.create( files=files_from_dir("financial_skill"), ) print(f"Created skill: {skill.id}") print(f"Latest version: {skill.latest_version}") ``` ```typescript TypeScript import { toFile } from "@anthropic-ai/sdk"; import fs from "node:fs"; // ... const client = new Anthropic(); // Option 1: Using a zip file const skillFromZip = await client.beta.skills.create({ files: [await toFile(fs.createReadStream("example_skill.zip"), "example_skill.zip")] }); // Option 2: Using individual file objects const skill = await client.beta.skills.create({ files: [ await toFile(fs.createReadStream("financial_skill/SKILL.md"), "financial_skill/SKILL.md", { type: "text/markdown" }), await toFile( fs.createReadStream("financial_skill/analyze.py"), "financial_skill/analyze.py", { type: "text/x-python" } ) ] }); console.log(`Created skill: ${skill.id}`); console.log(`Latest version: ${skill.latest_version}`); ``` ```csharp C# using Anthropic.Core; // ... AnthropicClient client = new(); // Option 1: Using a zip file var parameters = new SkillCreateParams { Files = [File.OpenRead("example_skill.zip")], }; var skill = await client.Beta.Skills.Create(parameters); // Option 2: Using individual files (path-qualified filenames preserve the Skill's directory layout) var parameters2 = new SkillCreateParams { Files = [ new BinaryContent { Stream = File.OpenRead("financial_skill/SKILL.md"), FileName = "financial_skill/SKILL.md", }, new BinaryContent { Stream = File.OpenRead("financial_skill/analyze.py"), FileName = "financial_skill/analyze.py", }, ], }; var skill2 = await client.Beta.Skills.Create(parameters2); Console.WriteLine($"Created skill: {skill.ID}"); Console.WriteLine($"Latest version: {skill.LatestVersion}"); Console.WriteLine($"Created skill 2: {skill2.ID}"); ``` ```go Go client := anthropic.NewClient() // Option 1: Using a zip file zipFile, err := os.Open("example_skill.zip") if err != nil { log.Fatal(err) } defer zipFile.Close() skill, err := client.Beta.Skills.New(context.TODO(), anthropic.BetaSkillNewParams{ Files: []io.Reader{zipFile}, }) if err != nil { log.Fatal(err) } // Option 2: Using individual files skillMd, err := os.Open("financial_skill/SKILL.md") if err != nil { log.Fatal(err) } defer skillMd.Close() analyzePy, err := os.Open("financial_skill/analyze.py") if err != nil { log.Fatal(err) } defer analyzePy.Close() skill2, err := client.Beta.Skills.New(context.TODO(), anthropic.BetaSkillNewParams{ Files: []io.Reader{ anthropic.File(skillMd, "financial_skill/SKILL.md", "text/markdown"), anthropic.File(analyzePy, "financial_skill/analyze.py", "text/x-python"), }, }) if err != nil { log.Fatal(err) } fmt.Printf("Created skill: %s\n", skill.ID) fmt.Printf("Latest version: %s\n", skill.LatestVersion) fmt.Printf("Created skill 2: %s\n", skill2.ID) ``` ```java Java import com.anthropic.core.MultipartField; import com.anthropic.models.beta.skills.SkillCreateParams; import com.anthropic.models.beta.skills.SkillCreateResponse; // ... void main() throws Exception { // ... AnthropicClient client = AnthropicOkHttpClient.fromEnv(); // Option 1: Using a zip file SkillCreateParams params = SkillCreateParams.builder() .addFile(MultipartField.builder() .value(Files.newInputStream(Path.of("example_skill.zip"))) .filename("example_skill.zip") .contentType("application/zip") .build()) .build(); SkillCreateResponse skill = client.beta().skills().create(params); // Option 2: Using individual files (path-qualified filenames preserve the Skill's directory layout) SkillCreateParams params2 = SkillCreateParams.builder() .addFile(MultipartField.builder() .value(Files.newInputStream(Path.of("financial_skill/SKILL.md"))) .filename("financial_skill/SKILL.md") .contentType("text/markdown") .build()) .addFile(MultipartField.builder() .value(Files.newInputStream(Path.of("financial_skill/analyze.py"))) .filename("financial_skill/analyze.py") .contentType("text/x-python") .build()) .build(); SkillCreateResponse skill2 = client.beta().skills().create(params2); System.out.println("Created skill: " + skill.id()); System.out.println("Latest version: " + skill.latestVersion().orElseThrow()); System.out.println("Created skill 2: " + skill2.id()); } ``` ```php PHP use Anthropic\Core\FileParam; // ... $client = new Client(); // Option 1: Using a zip file $skill = $client->beta->skills->create( files: [ FileParam::fromResource(fopen('example_skill.zip', 'r')) ], ); // Option 2: Using individual files $skill = $client->beta->skills->create( files: [ FileParam::fromResource(fopen('financial_skill/SKILL.md', 'r'), 'financial_skill/SKILL.md', 'text/markdown'), FileParam::fromResource(fopen('financial_skill/analyze.py', 'r'), 'financial_skill/analyze.py', 'text/x-python') ], ); echo "Created skill: {$skill->id}\n"; echo "Latest version: {$skill->latestVersion}\n"; ``` ```ruby Ruby client = Anthropic::Client.new # Option 1: Using a zip file skill = client.beta.skills.create( files: [ File.open("example_skill.zip", "rb") ] ) # Option 2: Using individual files skill = client.beta.skills.create( files: [ Anthropic::FilePart.new( Pathname("financial_skill/SKILL.md"), filename: "financial_skill/SKILL.md", content_type: "text/markdown" ), Anthropic::FilePart.new( Pathname("financial_skill/analyze.py"), filename: "financial_skill/analyze.py", content_type: "text/x-python" ) ] ) puts "Created skill: #{skill.id}" puts "Latest version: #{skill.latest_version}" ``` **Requirements:** * Must include a `SKILL.md` file at the top level * All files must specify a common root directory in their paths * The top-level directory name must match the `name` in `SKILL.md` frontmatter (case and underscore insensitive: `Financial_Skill` matches `financial-skill`) * `display_title` is optional: when omitted, it derives from the `SKILL.md` `name`; an explicit value must be unique among the custom skills in your workspace * Total upload size must be under 30 MB (uncompressed) * YAML frontmatter requirements: * `name`: Maximum 64 characters, lowercase letters/numbers/hyphens only, no XML tags, no reserved words ("anthropic", "claude") * `description`: Maximum 1024 characters, non-empty, no XML tags For complete request/response schemas, see the [Create Skill API reference](https://platform.claude.com/docs/en/api/beta/skills/create). ### Listing Skills Retrieve all Skills available to your workspace, including both Anthropic pre-built Skills and your custom Skills. Use the `source` parameter to filter by skill type: ```bash cURL # List all Skills curl "https://api.anthropic.com/v1/skills" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: skills-2025-10-02" # List only custom Skills curl "https://api.anthropic.com/v1/skills?source=custom" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: skills-2025-10-02" ``` ```bash CLI # List all Skills ant beta:skills list # List only custom Skills ant beta:skills list --source custom ``` ```python Python client = anthropic.Anthropic() # List all Skills for skill in client.beta.skills.list(): print(f"{skill.id}: {skill.display_title} (source: {skill.source})") # List only custom Skills custom_skills = client.beta.skills.list(source="custom") ``` ```typescript TypeScript const client = new Anthropic(); // List all Skills for await (const skill of client.beta.skills.list()) { console.log(`${skill.id}: ${skill.display_title} (source: ${skill.source})`); } // List only custom Skills const customSkills = await client.beta.skills.list({ source: "custom" }); ``` ```csharp C# AnthropicClient client = new(); // List all Skills await foreach (var skill in (await client.Beta.Skills.List()).Paginate()) { Console.WriteLine($"{skill.ID}: {skill.DisplayTitle} (source: {skill.Source})"); } // List only custom Skills var customSkills = await client.Beta.Skills.List(new SkillListParams { Source = "custom" }); ``` ```go Go client := anthropic.NewClient() // List all Skills skills := client.Beta.Skills.ListAutoPaging(context.TODO(), anthropic.BetaSkillListParams{}) for skills.Next() { skill := skills.Current() fmt.Printf("%s: %s (source: %s)\n", skill.ID, skill.DisplayTitle, skill.Source) } if skills.Err() != nil { log.Fatal(skills.Err()) } // List only custom Skills customSkills := client.Beta.Skills.ListAutoPaging(context.TODO(), anthropic.BetaSkillListParams{ Source: anthropic.String("custom"), }) for customSkills.Next() { skill := customSkills.Current() fmt.Printf("%s: %s (source: %s)\n", skill.ID, skill.DisplayTitle, skill.Source) } if customSkills.Err() != nil { log.Fatal(customSkills.Err()) } ``` ```java Java import com.anthropic.models.beta.skills.SkillListParams; import com.anthropic.models.beta.skills.SkillListPage; import com.anthropic.models.beta.skills.SkillListResponse; // ... void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); // List Skills (first page) SkillListPage skills = client.beta().skills().list(); for (SkillListResponse skill : skills.data()) { System.out.println(skill.id() + ": " + skill.displayTitle().orElseThrow() + " (source: " + skill.source() + ")"); } // List only custom Skills SkillListParams customParams = SkillListParams.builder() .source("custom") .build(); SkillListPage customSkills = client.beta().skills().list(customParams); } ``` ```php PHP $client = new Client(); // List Skills (first page) $skills = $client->beta->skills->list(); foreach ($skills->data as $skill) { echo "{$skill->id}: {$skill->displayTitle} (source: {$skill->source})\n"; } // List only custom Skills $customSkills = $client->beta->skills->list( source: 'custom', ); ``` ```ruby Ruby client = Anthropic::Client.new # List all Skills client.beta.skills.list.auto_paging_each do |skill| puts "#{skill.id}: #{skill.display_title} (source: #{skill.source})" end # List only custom Skills custom_skills = client.beta.skills.list( source: "custom" ) ``` See the [List Skills API reference](https://platform.claude.com/docs/en/api/beta/skills/list) for pagination and filtering options. ### Retrieving a Skill Get details about a specific Skill: ```bash cURL curl "https://api.anthropic.com/v1/skills/skill_01AbCdEfGhIjKlMnOpQrStUv" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: skills-2025-10-02" ``` ```bash CLI ant beta:skills retrieve \ --skill-id skill_01AbCdEfGhIjKlMnOpQrStUv ``` ```python Python client = anthropic.Anthropic() skill = client.beta.skills.retrieve(skill_id="skill_01AbCdEfGhIjKlMnOpQrStUv") print(f"Skill: {skill.display_title}") print(f"Latest version: {skill.latest_version}") print(f"Created: {skill.created_at}") ``` ```typescript TypeScript const client = new Anthropic(); const skill = await client.beta.skills.retrieve("skill_01AbCdEfGhIjKlMnOpQrStUv"); console.log(`Skill: ${skill.display_title}`); console.log(`Latest version: ${skill.latest_version}`); console.log(`Created: ${skill.created_at}`); ``` ```csharp C# AnthropicClient client = new(); var skill = await client.Beta.Skills.Retrieve("skill_01AbCdEfGhIjKlMnOpQrStUv"); Console.WriteLine($"Skill: {skill.DisplayTitle}"); Console.WriteLine($"Latest version: {skill.LatestVersion}"); Console.WriteLine($"Created: {skill.CreatedAt}"); ``` ```go Go client := anthropic.NewClient() skill, err := client.Beta.Skills.Get( context.TODO(), "skill_01AbCdEfGhIjKlMnOpQrStUv", anthropic.BetaSkillGetParams{}, ) if err != nil { log.Fatal(err) } fmt.Printf("Skill: %s\n", skill.DisplayTitle) fmt.Printf("Latest version: %s\n", skill.LatestVersion) fmt.Printf("Created: %s\n", skill.CreatedAt) ``` ```java Java import com.anthropic.models.beta.skills.SkillRetrieveResponse; // ... void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); SkillRetrieveResponse skill = client.beta().skills().retrieve("skill_01AbCdEfGhIjKlMnOpQrStUv"); System.out.println("Skill: " + skill.displayTitle().orElseThrow()); System.out.println("Latest version: " + skill.latestVersion().orElseThrow()); System.out.println("Created: " + skill.createdAt()); } ``` ```php PHP $client = new Client(); $skill = $client->beta->skills->retrieve( skillID: 'skill_01AbCdEfGhIjKlMnOpQrStUv', ); echo "Skill: " . $skill->displayTitle . "\n"; echo "Latest version: " . $skill->latestVersion . "\n"; echo "Created: " . $skill->createdAt . "\n"; ``` ```ruby Ruby client = Anthropic::Client.new skill = client.beta.skills.retrieve("skill_01AbCdEfGhIjKlMnOpQrStUv") puts "Skill: #{skill.display_title}" puts "Latest version: #{skill.latest_version}" puts "Created: #{skill.created_at}" ``` ### Deleting a Skill To delete a Skill, you must first delete all its versions: ```bash cURL # Step 1: List the versions, then delete each one curl "https://api.anthropic.com/v1/skills/skill_01AbCdEfGhIjKlMnOpQrStUv/versions" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: skills-2025-10-02" # Repeat for each version the list returned curl -X DELETE "https://api.anthropic.com/v1/skills/skill_01AbCdEfGhIjKlMnOpQrStUv/versions/1759178010641129" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: skills-2025-10-02" # Step 2: Delete the Skill curl -X DELETE "https://api.anthropic.com/v1/skills/skill_01AbCdEfGhIjKlMnOpQrStUv" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: skills-2025-10-02" ``` ```bash CLI # Step 1: List the versions, then delete each one ant beta:skills:versions list \ --skill-id skill_01AbCdEfGhIjKlMnOpQrStUv \ --transform version \ --raw-output # Repeat for each version id the list returned ant beta:skills:versions delete \ --skill-id skill_01AbCdEfGhIjKlMnOpQrStUv \ --version 1759178010641129 >/dev/null # Step 2: Delete the Skill ant beta:skills delete \ --skill-id skill_01AbCdEfGhIjKlMnOpQrStUv >/dev/null ``` ```python Python client = anthropic.Anthropic() # Step 1: Delete all versions for version in client.beta.skills.versions.list( skill_id="skill_01AbCdEfGhIjKlMnOpQrStUv" ): client.beta.skills.versions.delete( skill_id="skill_01AbCdEfGhIjKlMnOpQrStUv", version=version.version, ) # Step 2: Delete the Skill client.beta.skills.delete(skill_id="skill_01AbCdEfGhIjKlMnOpQrStUv") ``` ```typescript TypeScript const client = new Anthropic(); // Step 1: Delete all versions for await (const version of client.beta.skills.versions.list( "skill_01AbCdEfGhIjKlMnOpQrStUv" )) { await client.beta.skills.versions.delete(version.version, { skill_id: "skill_01AbCdEfGhIjKlMnOpQrStUv" }); } // Step 2: Delete the Skill await client.beta.skills.delete("skill_01AbCdEfGhIjKlMnOpQrStUv"); ``` ```csharp C# using Anthropic.Models.Beta.Skills.Versions; // ... AnthropicClient client = new(); // Step 1: Delete all versions await foreach (var version in (await client.Beta.Skills.Versions.List("skill_01AbCdEfGhIjKlMnOpQrStUv")).Paginate()) { await client.Beta.Skills.Versions.Delete( version.Version, new VersionDeleteParams { SkillID = "skill_01AbCdEfGhIjKlMnOpQrStUv" } ); } // Step 2: Delete the Skill await client.Beta.Skills.Delete("skill_01AbCdEfGhIjKlMnOpQrStUv"); ``` ```go Go client := anthropic.NewClient() // Step 1: Delete all versions versions := client.Beta.Skills.Versions.ListAutoPaging( context.TODO(), "skill_01AbCdEfGhIjKlMnOpQrStUv", anthropic.BetaSkillVersionListParams{}, ) for versions.Next() { version := versions.Current() _, err := client.Beta.Skills.Versions.Delete( context.TODO(), version.Version, anthropic.BetaSkillVersionDeleteParams{ SkillID: "skill_01AbCdEfGhIjKlMnOpQrStUv", }, ) if err != nil { log.Fatal(err) } } if versions.Err() != nil { log.Fatal(versions.Err()) } // Step 2: Delete the Skill _, err := client.Beta.Skills.Delete( context.TODO(), "skill_01AbCdEfGhIjKlMnOpQrStUv", anthropic.BetaSkillDeleteParams{}, ) if err != nil { log.Fatal(err) } ``` ```java Java import com.anthropic.models.beta.skills.versions.VersionListPage; import com.anthropic.models.beta.skills.versions.VersionDeleteParams; // ... void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); // Step 1: Delete all versions VersionListPage versions = client.beta().skills().versions().list("skill_01AbCdEfGhIjKlMnOpQrStUv"); for (var version : versions.autoPager()) { client.beta().skills().versions().delete( version.version(), VersionDeleteParams.builder() .skillId("skill_01AbCdEfGhIjKlMnOpQrStUv") .build() ); } // Step 2: Delete the Skill client.beta().skills().delete("skill_01AbCdEfGhIjKlMnOpQrStUv"); } ``` ```php PHP $client = new Client(); // Step 1: Delete all versions $versions = $client->beta->skills->versions->list( skillID: 'skill_01AbCdEfGhIjKlMnOpQrStUv', ); foreach ($versions->pagingEachItem() as $version) { $client->beta->skills->versions->delete( skillID: 'skill_01AbCdEfGhIjKlMnOpQrStUv', version: $version->version, ); } // Step 2: Delete the Skill $client->beta->skills->delete( skillID: 'skill_01AbCdEfGhIjKlMnOpQrStUv', ); ``` ```ruby Ruby client = Anthropic::Client.new # Step 1: Delete all versions client.beta.skills.versions.list("skill_01AbCdEfGhIjKlMnOpQrStUv").auto_paging_each do |version| client.beta.skills.versions.delete( version.version, skill_id: "skill_01AbCdEfGhIjKlMnOpQrStUv" ) end # Step 2: Delete the Skill client.beta.skills.delete("skill_01AbCdEfGhIjKlMnOpQrStUv") ``` Attempting to delete a Skill with existing versions returns a 400 error. ### Versioning Skills support versioning to manage updates safely: **Anthropic Skills:** * Versions use date format: `20251013` * New versions released as updates are made * Specify exact versions for stability **Custom Skills:** * Auto-generated epoch timestamps: `1759178010641129` * Use `"latest"` to always get the most recent version * Create new versions when updating Skill files A new version is a complete snapshot, not a delta: upload the Skill's full file set each time, under the same top-level directory name used at creation. Files you omit are not carried over. The following examples re-upload the complete `financial_skill/` bundle from [Creating a Skill](https://platform.claude.com/docs/en/build-with-claude/skills-guide#creating-a-skill). ```bash cURL # Create a new version NEW_VERSION=$(curl -X POST "https://api.anthropic.com/v1/skills/skill_01AbCdEfGhIjKlMnOpQrStUv/versions" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: skills-2025-10-02" \ -F "files[]=@financial_skill/SKILL.md;filename=financial_skill/SKILL.md" \ -F "files[]=@financial_skill/analyze.py;filename=financial_skill/analyze.py") VERSION_NUMBER=$(echo "$NEW_VERSION" | jq -r '.version') # Use specific version curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: code-execution-2025-08-25,skills-2025-10-02" \ -H "content-type: application/json" \ -d "{ \"model\": \"claude-opus-5\", \"max_tokens\": 4096, \"container\": { \"skills\": [{ \"type\": \"custom\", \"skill_id\": \"skill_01AbCdEfGhIjKlMnOpQrStUv\", \"version\": \"$VERSION_NUMBER\" }] }, \"messages\": [{\"role\": \"user\", \"content\": \"Use updated Skill\"}], \"tools\": [{\"type\": \"code_execution_20250825\", \"name\": \"code_execution\"}] }" # Use latest version curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: code-execution-2025-08-25,skills-2025-10-02" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 4096, "container": { "skills": [{ "type": "custom", "skill_id": "skill_01AbCdEfGhIjKlMnOpQrStUv", "version": "latest" }] }, "messages": [{"role": "user", "content": "Use latest Skill version"}], "tools": [{"type": "code_execution_20250825", "name": "code_execution"}] }' ``` ```bash CLI # Create a new version VERSION_NUMBER=$(ant beta:skills:versions create \ --skill-id skill_01AbCdEfGhIjKlMnOpQrStUv \ --file financial_skill.zip \ --transform version \ --raw-output) # Use specific version ant beta:messages create \ --beta code-execution-2025-08-25,skills-2025-10-02 <builder() .value(Files.newInputStream(Path.of("financial_skill.zip"))) .filename("financial_skill.zip") .contentType("application/zip") .build()) .build(); VersionCreateResponse newVersion = client.beta().skills().versions() .create("skill_01AbCdEfGhIjKlMnOpQrStUv", versionParams); // Use specific version MessageCreateParams specificVersionParams = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(4096L) .addBeta("code-execution-2025-08-25") .addBeta("skills-2025-10-02") .container(BetaContainerParams.builder() .addSkill(BetaSkillParams.builder() .type(BetaSkillParams.Type.CUSTOM) .skillId("skill_01AbCdEfGhIjKlMnOpQrStUv") .version(newVersion.version()) .build()) .build()) .addUserMessage("Use updated Skill") .addTool(BetaCodeExecutionTool20250825.builder().build()) .build(); BetaMessage response = client.beta().messages().create(specificVersionParams); System.out.println(response); // Use latest version MessageCreateParams latestVersionParams = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(4096L) .addBeta("code-execution-2025-08-25") .addBeta("skills-2025-10-02") .container(BetaContainerParams.builder() .addSkill(BetaSkillParams.builder() .type(BetaSkillParams.Type.CUSTOM) .skillId("skill_01AbCdEfGhIjKlMnOpQrStUv") .version("latest") .build()) .build()) .addUserMessage("Use latest Skill version") .addTool(BetaCodeExecutionTool20250825.builder().build()) .build(); BetaMessage latestResponse = client.beta().messages().create(latestVersionParams); System.out.println(latestResponse); ``` ```php PHP use Anthropic\Core\FileParam; // ... $client = new Client(); // Create a new version $newVersion = $client->beta->skills->versions->create( skillID: 'skill_01AbCdEfGhIjKlMnOpQrStUv', files: [ FileParam::fromResource(fopen('financial_skill/SKILL.md', 'r'), 'financial_skill/SKILL.md', 'text/markdown'), FileParam::fromResource(fopen('financial_skill/analyze.py', 'r'), 'financial_skill/analyze.py', 'text/x-python'), ], ); // Use specific version $response = $client->beta->messages->create( maxTokens: 4096, messages: [['role' => 'user', 'content' => 'Use updated Skill']], model: 'claude-opus-5', betas: ['code-execution-2025-08-25', 'skills-2025-10-02'], container: [ 'skills' => [[ 'type' => 'custom', 'skill_id' => 'skill_01AbCdEfGhIjKlMnOpQrStUv', 'version' => $newVersion->version ]] ], tools: [['type' => 'code_execution_20250825', 'name' => 'code_execution']] ); echo $response; // Use latest version $latestResponse = $client->beta->messages->create( maxTokens: 4096, messages: [['role' => 'user', 'content' => 'Use latest Skill version']], model: 'claude-opus-5', betas: ['code-execution-2025-08-25', 'skills-2025-10-02'], container: [ 'skills' => [[ 'type' => 'custom', 'skill_id' => 'skill_01AbCdEfGhIjKlMnOpQrStUv', 'version' => 'latest' ]] ], tools: [['type' => 'code_execution_20250825', 'name' => 'code_execution']] ); echo $latestResponse; ``` ```ruby Ruby client = Anthropic::Client.new # Create a new version new_version = client.beta.skills.versions.create( "skill_01AbCdEfGhIjKlMnOpQrStUv", files: [ Anthropic::FilePart.new( Pathname("financial_skill/SKILL.md"), filename: "financial_skill/SKILL.md", content_type: "text/markdown" ), Anthropic::FilePart.new( Pathname("financial_skill/analyze.py"), filename: "financial_skill/analyze.py", content_type: "text/x-python" ) ] ) # Use specific version response = client.beta.messages.create( model: "claude-opus-5", max_tokens: 4096, betas: ["code-execution-2025-08-25", "skills-2025-10-02"], container: { skills: [{ type: "custom", skill_id: "skill_01AbCdEfGhIjKlMnOpQrStUv", version: new_version.version }] }, messages: [{ role: "user", content: "Use updated Skill" }], tools: [{ type: "code_execution_20250825", name: "code_execution" }] ) puts response # Use latest version latest_response = client.beta.messages.create( model: "claude-opus-5", max_tokens: 4096, betas: ["code-execution-2025-08-25", "skills-2025-10-02"], container: { skills: [{ type: "custom", skill_id: "skill_01AbCdEfGhIjKlMnOpQrStUv", version: "latest" }] }, messages: [{ role: "user", content: "Use latest Skill version" }], tools: [{ type: "code_execution_20250825", name: "code_execution" }] ) puts latest_response ``` See the [Create Skill Version API reference](https://platform.claude.com/docs/en/api/beta/skills/versions/create) for complete details. *** ## How Skills are loaded When you specify Skills in a container: 1. **Metadata discovery:** Claude sees metadata for each Skill (name, description) in the system prompt. 2. **File loading:** Skill files are copied into the container at `/skills/{skill-name}/`. The directory is the Skill's name (`pptx` for an Anthropic Skill, the `SKILL.md` `name` for a custom Skill), not its `skill_01...` ID. 3. **Automatic use:** Claude automatically loads and uses Skills when relevant to your request. 4. **Composition:** Multiple Skills compose together for complex workflows. Claude loads full Skill instructions only when needed. *** ## Use cases Skills fit both organizational and personal work. Organizations use them to apply brand formatting to documents, structure notes and reports around company templates, and run company-specific analytical procedures. Individuals use them for custom document templates, specialized data pipelines, and code generation or deployment conventions. ### Example: financial modeling Combine Excel and custom DCF analysis Skills: ```bash cURL # Create custom DCF analysis Skill DCF_SKILL=$(curl -X POST "https://api.anthropic.com/v1/skills" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: skills-2025-10-02" \ -F "files[]=@dcf_skill/SKILL.md;filename=dcf_skill/SKILL.md") DCF_SKILL_ID=$(echo "$DCF_SKILL" | jq -r '.id') # Use with Excel to create financial model curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: code-execution-2025-08-25,skills-2025-10-02" \ -H "content-type: application/json" \ -d "{ \"model\": \"claude-opus-5\", \"max_tokens\": 4096, \"container\": { \"skills\": [ { \"type\": \"anthropic\", \"skill_id\": \"xlsx\", \"version\": \"latest\" }, { \"type\": \"custom\", \"skill_id\": \"$DCF_SKILL_ID\", \"version\": \"latest\" } ] }, \"messages\": [{ \"role\": \"user\", \"content\": \"Build a DCF valuation model for a SaaS company\" }], \"tools\": [{ \"type\": \"code_execution_20250825\", \"name\": \"code_execution\" }] }" ``` ```bash CLI # Create custom DCF analysis Skill DCF_SKILL_ID=$(ant beta:skills create \ --file dcf_skill.zip \ --transform id \ --raw-output) # Use with Excel to create financial model ant beta:messages create \ --beta code-execution-2025-08-25,skills-2025-10-02 <beta->messages->create( maxTokens: 4096, messages: [ ['role' => 'user', 'content' => 'Build a DCF valuation model for a SaaS company'] ], model: 'claude-opus-5', betas: ['code-execution-2025-08-25', 'skills-2025-10-02'], container: [ 'skills' => [ ['type' => 'anthropic', 'skill_id' => 'xlsx', 'version' => 'latest'], ['type' => 'custom', 'skill_id' => $dcfSkillId, 'version' => 'latest'] ] ], tools: [ ['type' => 'code_execution_20250825', 'name' => 'code_execution'] ] ); echo $message; ``` ```ruby Ruby client = Anthropic::Client.new # Create custom DCF analysis Skill dcf_skill = client.beta.skills.create( files: [ Anthropic::FilePart.new( Pathname("dcf_skill/SKILL.md"), filename: "dcf_skill/SKILL.md", content_type: "text/markdown" ) ] ) # Use with Excel to create financial model response = client.beta.messages.create( model: "claude-opus-5", max_tokens: 4096, betas: ["code-execution-2025-08-25", "skills-2025-10-02"], container: { skills: [ { type: "anthropic", skill_id: "xlsx", version: "latest" }, { type: "custom", skill_id: dcf_skill.id, version: "latest" } ] }, messages: [ { role: "user", content: "Build a DCF valuation model for a SaaS company" } ], tools: [{ type: "code_execution_20250825", name: "code_execution" }] ) puts response ``` *** ## Limits and constraints ### Request limits * **Maximum Skills per request:** 8 * **Maximum Skill upload size:** 30 MB (all files combined, uncompressed) * **YAML frontmatter requirements:** * `name`: Maximum 64 characters, lowercase letters/numbers/hyphens only, no XML tags, no reserved words ("anthropic", "claude") * `description`: Maximum 1024 characters, non-empty, no XML tags ### Environment constraints Skills run in the code execution container with these limitations: * **No network access:** Cannot make external API calls * **No runtime package installation:** Only pre-installed packages available * **Isolated environment:** A fresh container is created unless you specify an existing container ID See [Code execution tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool) for available packages. *** ## Best practices ### When to use multiple Skills Combine Skills when tasks involve multiple document types or domains: **Good use cases:** * Data analysis (Excel) + presentation creation (PowerPoint) * Report generation (Word) + export to PDF * Custom domain logic + document generation **Avoid:** * Including unused Skills (impacts performance) ### Version management strategy The SDK tabs in this section show the `container` value to include in a Messages request. The cURL and CLI tabs show the full request. **For production:** pin a specific version, so Skill updates never change your deployed behavior. The version ID comes from the create-version response in [Versioning](https://platform.claude.com/docs/en/build-with-claude/skills-guide#versioning) or from the [List Skill Versions API](https://platform.claude.com/docs/en/api/beta/skills/versions/list). The ID is always a string: quote epoch-timestamp IDs in JSON or YAML. ```bash cURL # Pin to specific versions for stability curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: code-execution-2025-08-25,skills-2025-10-02" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 4096, "container": { "skills": [{ "type": "custom", "skill_id": "skill_01AbCdEfGhIjKlMnOpQrStUv", "version": "1759178010641129" }] }, "messages": [{"role": "user", "content": "Analyze the sales data"}], "tools": [{"type": "code_execution_20250825", "name": "code_execution"}] }' ``` ```bash CLI # Pin to specific versions for stability ant beta:messages create \ --beta code-execution-2025-08-25,skills-2025-10-02 < [[ 'type' => 'custom', 'skill_id' => 'skill_01AbCdEfGhIjKlMnOpQrStUv', 'version' => '1759178010641129' ]] ]; ``` ```ruby Ruby # Pin to specific versions for stability container = { skills: [{ type: "custom", skill_id: "skill_01AbCdEfGhIjKlMnOpQrStUv", version: "1759178010641129" }] } ``` **For development:** use `latest` to pick up the newest version automatically as you iterate. ```bash cURL # Use latest for active development curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: code-execution-2025-08-25,skills-2025-10-02" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 4096, "container": { "skills": [{ "type": "custom", "skill_id": "skill_01AbCdEfGhIjKlMnOpQrStUv", "version": "latest" }] }, "messages": [{"role": "user", "content": "Analyze the sales data"}], "tools": [{"type": "code_execution_20250825", "name": "code_execution"}] }' ``` ```bash CLI # Use latest for active development ant beta:messages create \ --beta code-execution-2025-08-25,skills-2025-10-02 < [[ 'type' => 'custom', 'skill_id' => 'skill_01AbCdEfGhIjKlMnOpQrStUv', 'version' => 'latest' ]] ]; ``` ```ruby Ruby # Use latest for active development container = { skills: [{ type: "custom", skill_id: "skill_01AbCdEfGhIjKlMnOpQrStUv", version: "latest" }] } ``` ### Prompt caching considerations If you use [Prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching), changing the Skills list in your container breaks the cache. Skills render into the system prompt in a fixed order, so the same list produces the same cacheable prefix: ```bash cURL # Skills render into the system prompt in a fixed, cache-friendly order curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: code-execution-2025-08-25,skills-2025-10-02" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 4096, "container": { "skills": [ {"type": "anthropic", "skill_id": "xlsx", "version": "latest"} ] }, "messages": [{"role": "user", "content": "Analyze sales data"}], "tools": [{"type": "code_execution_20250825", "name": "code_execution"}] }' # Changing the Skills list ([xlsx] vs [xlsx, pptx]) changes the prefix: a cache miss, while an identical list is a cache hit curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: code-execution-2025-08-25,skills-2025-10-02" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 4096, "container": { "skills": [ {"type": "anthropic", "skill_id": "xlsx", "version": "latest"}, {"type": "anthropic", "skill_id": "pptx", "version": "latest"} ] }, "messages": [{"role": "user", "content": "Create a presentation"}], "tools": [{"type": "code_execution_20250825", "name": "code_execution"}] }' ``` ```bash CLI # Skills render into the system prompt in a fixed, cache-friendly order ant beta:messages create \ --beta code-execution-2025-08-25,skills-2025-10-02 <<'YAML' model: claude-opus-5 max_tokens: 4096 container: skills: - type: anthropic skill_id: xlsx version: latest messages: - role: user content: Analyze sales data tools: - type: code_execution_20250825 name: code_execution YAML # Changing the Skills list ([xlsx] vs [xlsx, pptx]) changes the prefix: a cache miss, while an identical list is a cache hit ant beta:messages create \ --beta code-execution-2025-08-25,skills-2025-10-02 <<'YAML' model: claude-opus-5 max_tokens: 4096 container: skills: - type: anthropic skill_id: xlsx version: latest - type: anthropic skill_id: pptx version: latest messages: - role: user content: Create a presentation tools: - type: code_execution_20250825 name: code_execution YAML ``` ```python Python client = anthropic.Anthropic() # Skills render into the system prompt in a fixed, cache-friendly order response1 = client.beta.messages.create( model="claude-opus-5", max_tokens=4096, betas=[ "code-execution-2025-08-25", "skills-2025-10-02", ], container={ "skills": [{"type": "anthropic", "skill_id": "xlsx", "version": "latest"}] }, messages=[{"role": "user", "content": "Analyze sales data"}], tools=[{"type": "code_execution_20250825", "name": "code_execution"}], ) # Changing the Skills list ([xlsx] vs [xlsx, pptx]) changes the prefix: a cache miss, while an identical list is a cache hit response2 = client.beta.messages.create( model="claude-opus-5", max_tokens=4096, betas=[ "code-execution-2025-08-25", "skills-2025-10-02", ], container={ "skills": [ {"type": "anthropic", "skill_id": "xlsx", "version": "latest"}, { "type": "anthropic", "skill_id": "pptx", "version": "latest", }, # prefix change: cache miss ] }, messages=[{"role": "user", "content": "Create a presentation"}], tools=[{"type": "code_execution_20250825", "name": "code_execution"}], ) ``` ```typescript TypeScript const client = new Anthropic(); // Skills render into the system prompt in a fixed, cache-friendly order const response1 = await client.beta.messages.create({ model: "claude-opus-5", max_tokens: 4096, betas: ["code-execution-2025-08-25", "skills-2025-10-02"], container: { skills: [{ type: "anthropic", skill_id: "xlsx", version: "latest" }] }, messages: [{ role: "user", content: "Analyze sales data" }], tools: [{ type: "code_execution_20250825", name: "code_execution" }] }); // Changing the Skills list ([xlsx] vs [xlsx, pptx]) changes the prefix: a cache miss, while an identical list is a cache hit const response2 = await client.beta.messages.create({ model: "claude-opus-5", max_tokens: 4096, betas: ["code-execution-2025-08-25", "skills-2025-10-02"], container: { skills: [ { type: "anthropic", skill_id: "xlsx", version: "latest" }, { type: "anthropic", skill_id: "pptx", version: "latest" } // prefix change: cache miss ] }, messages: [{ role: "user", content: "Create a presentation" }], tools: [{ type: "code_execution_20250825", name: "code_execution" }] }); ``` ```csharp C# AnthropicClient client = new(); // Skills render into the system prompt in a fixed, cache-friendly order var parameters1 = new MessageCreateParams { Model = "claude-opus-5", MaxTokens = 4096, Betas = ["code-execution-2025-08-25", "skills-2025-10-02"], Container = new BetaContainerParams { Skills = [ new BetaSkillParams { Type = BetaSkillParamsType.Anthropic, SkillID = "xlsx", Version = "latest", }, ], }, Messages = [new() { Role = Role.User, Content = "Analyze sales data" }], Tools = [new BetaCodeExecutionTool20250825()], }; var response1 = await client.Beta.Messages.Create(parameters1); Console.WriteLine(response1); // Different Skill set ([xlsx] vs [xlsx, pptx]) = a different prefix: a cache miss (an identical set is a cache hit) var parameters2 = new MessageCreateParams { Model = "claude-opus-5", MaxTokens = 4096, Betas = ["code-execution-2025-08-25", "skills-2025-10-02"], Container = new BetaContainerParams { Skills = [ new BetaSkillParams { Type = BetaSkillParamsType.Anthropic, SkillID = "xlsx", Version = "latest", }, new BetaSkillParams { Type = BetaSkillParamsType.Anthropic, SkillID = "pptx", Version = "latest", }, ], }, Messages = [new() { Role = Role.User, Content = "Create a presentation" }], Tools = [new BetaCodeExecutionTool20250825()], }; var response2 = await client.Beta.Messages.Create(parameters2); Console.WriteLine(response2); ``` ```go Go client := anthropic.NewClient() // Skills render into the system prompt in a fixed, cache-friendly order response1, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{ Model: "claude-opus-5", MaxTokens: 4096, Betas: []anthropic.AnthropicBeta{ "code-execution-2025-08-25", anthropic.AnthropicBetaSkills2025_10_02, }, Container: anthropic.BetaMessageNewParamsContainerUnion{ OfContainers: &anthropic.BetaContainerParams{ Skills: []anthropic.BetaSkillParams{ { Type: anthropic.BetaSkillParamsTypeAnthropic, SkillID: "xlsx", Version: anthropic.String("latest"), }, }, }, }, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Analyze sales data")), }, Tools: []anthropic.BetaToolUnionParam{ {OfCodeExecutionTool20250825: &anthropic.BetaCodeExecutionTool20250825Param{}}, }, }) if err != nil { log.Fatal(err) } fmt.Println(response1) // Changing the Skills list ([xlsx] vs [xlsx, pptx]) changes the prefix: a cache miss, while an identical list is a cache hit response2, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{ Model: "claude-opus-5", MaxTokens: 4096, Betas: []anthropic.AnthropicBeta{ "code-execution-2025-08-25", anthropic.AnthropicBetaSkills2025_10_02, }, Container: anthropic.BetaMessageNewParamsContainerUnion{ OfContainers: &anthropic.BetaContainerParams{ Skills: []anthropic.BetaSkillParams{ { Type: anthropic.BetaSkillParamsTypeAnthropic, SkillID: "xlsx", Version: anthropic.String("latest"), }, { Type: anthropic.BetaSkillParamsTypeAnthropic, SkillID: "pptx", Version: anthropic.String("latest"), }, }, }, }, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Create a presentation")), }, Tools: []anthropic.BetaToolUnionParam{ {OfCodeExecutionTool20250825: &anthropic.BetaCodeExecutionTool20250825Param{}}, }, }) if err != nil { log.Fatal(err) } fmt.Println(response2) ``` ```java Java import com.anthropic.models.beta.messages.BetaContainerParams; import com.anthropic.models.beta.messages.BetaSkillParams; import com.anthropic.models.beta.messages.BetaCodeExecutionTool20250825; // ... void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); // Skills render into the system prompt in a fixed, cache-friendly order MessageCreateParams params1 = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(4096L) .addBeta("code-execution-2025-08-25") .addBeta("skills-2025-10-02") .container(BetaContainerParams.builder() .skills(List.of( BetaSkillParams.builder() .type(BetaSkillParams.Type.ANTHROPIC) .skillId("xlsx") .version("latest") .build() )) .build()) .addUserMessage("Analyze sales data") .addTool(BetaCodeExecutionTool20250825.builder().build()) .build(); BetaMessage response1 = client.beta().messages().create(params1); System.out.println(response1); // Changing the Skills list ([xlsx] vs [xlsx, pptx]) changes the prefix: a cache miss, while an identical list is a cache hit MessageCreateParams params2 = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(4096L) .addBeta("code-execution-2025-08-25") .addBeta("skills-2025-10-02") .container(BetaContainerParams.builder() .skills(List.of( BetaSkillParams.builder() .type(BetaSkillParams.Type.ANTHROPIC) .skillId("xlsx") .version("latest") .build(), BetaSkillParams.builder() .type(BetaSkillParams.Type.ANTHROPIC) .skillId("pptx") .version("latest") .build() )) .build()) .addUserMessage("Create a presentation") .addTool(BetaCodeExecutionTool20250825.builder().build()) .build(); BetaMessage response2 = client.beta().messages().create(params2); System.out.println(response2); } ``` ```php PHP $client = new Client(); // Skills render into the system prompt in a fixed, cache-friendly order $response1 = $client->beta->messages->create( maxTokens: 4096, messages: [ ['role' => 'user', 'content' => 'Analyze sales data'] ], model: 'claude-opus-5', betas: [ 'code-execution-2025-08-25', 'skills-2025-10-02', ], container: [ 'skills' => [ ['type' => 'anthropic', 'skill_id' => 'xlsx', 'version' => 'latest'] ] ], tools: [ ['type' => 'code_execution_20250825', 'name' => 'code_execution'] ] ); echo $response1; // Changing the Skills list ([xlsx] vs [xlsx, pptx]) changes the prefix: a cache miss, while an identical list is a cache hit $response2 = $client->beta->messages->create( maxTokens: 4096, messages: [ ['role' => 'user', 'content' => 'Create a presentation'] ], model: 'claude-opus-5', betas: [ 'code-execution-2025-08-25', 'skills-2025-10-02', ], container: [ 'skills' => [ ['type' => 'anthropic', 'skill_id' => 'xlsx', 'version' => 'latest'], ['type' => 'anthropic', 'skill_id' => 'pptx', 'version' => 'latest'] ] ], tools: [ ['type' => 'code_execution_20250825', 'name' => 'code_execution'] ] ); echo $response2; ``` ```ruby Ruby client = Anthropic::Client.new # Skills render into the system prompt in a fixed, cache-friendly order response1 = client.beta.messages.create( model: "claude-opus-5", max_tokens: 4096, betas: [ "code-execution-2025-08-25", "skills-2025-10-02", ], container: { skills: [{ type: "anthropic", skill_id: "xlsx", version: "latest" }] }, messages: [{ role: "user", content: "Analyze sales data" }], tools: [{ type: "code_execution_20250825", name: "code_execution" }] ) puts response1 # Changing the Skills list ([xlsx] vs [xlsx, pptx]) changes the prefix: a cache miss, while an identical list is a cache hit response2 = client.beta.messages.create( model: "claude-opus-5", max_tokens: 4096, betas: [ "code-execution-2025-08-25", "skills-2025-10-02", ], container: { skills: [ { type: "anthropic", skill_id: "xlsx", version: "latest" }, { type: "anthropic", skill_id: "pptx", version: "latest" } # prefix change: cache miss ] }, messages: [{ role: "user", content: "Create a presentation" }], tools: [{ type: "code_execution_20250825", name: "code_execution" }] ) puts response2 ``` For best caching performance, keep your Skills list, including its order, consistent across requests. Pinning custom Skill versions also helps: with `"latest"`, publishing a new version can invalidate the cached prefix if it changes the Skill's description. ### Error handling Handle Skill-related errors gracefully: ```bash cURL # This error-handling flow doesn't translate well to a one-off shell # command; one of the SDK options would be a better fit. A failing request # returns HTTP 400 with an error JSON whose .error.message names the # Skill problem. ``` ```bash CLI if ! RESULT=$(ant beta:messages create \ --beta code-execution-2025-08-25,skills-2025-10-02 \ --transform-error error.message \ --format-error yaml 2>&1 <<'YAML' model: claude-opus-5 max_tokens: 4096 container: skills: - type: custom skill_id: skill_01AbCdEfGhIjKlMnOpQrStUv version: latest messages: - role: user content: Process data tools: - type: code_execution_20250825 name: code_execution YAML ); then case "$RESULT" in *skill*) printf 'Skill error: %s\n' "$RESULT" # Handle skill-specific errors ;; *) printf '%s\n' "$RESULT" >&2 exit 1 ;; esac fi ``` ```python Python client = anthropic.Anthropic() try: response = client.beta.messages.create( model="claude-opus-5", max_tokens=4096, betas=["code-execution-2025-08-25", "skills-2025-10-02"], container={ "skills": [ { "type": "custom", "skill_id": "skill_01AbCdEfGhIjKlMnOpQrStUv", "version": "latest", } ] }, messages=[{"role": "user", "content": "Process data"}], tools=[{"type": "code_execution_20250825", "name": "code_execution"}], ) except anthropic.BadRequestError as e: if "skill" in str(e): print(f"Skill error: {e}") # Handle skill-specific errors else: raise ``` ```typescript TypeScript const client = new Anthropic(); try { const response = await client.beta.messages.create({ model: "claude-opus-5", max_tokens: 4096, betas: ["code-execution-2025-08-25", "skills-2025-10-02"], container: { skills: [ { type: "custom", skill_id: "skill_01AbCdEfGhIjKlMnOpQrStUv", version: "latest" } ] }, messages: [{ role: "user", content: "Process data" }], tools: [{ type: "code_execution_20250825", name: "code_execution" }] }); console.log(response); } catch (error) { if (error instanceof Anthropic.BadRequestError && error.message.includes("skill")) { console.error(`Skill error: ${error.message}`); // Handle skill-specific errors } else { throw error; } } ``` ```csharp C# using Anthropic.Exceptions; // ... AnthropicClient client = new(); try { var parameters = new MessageCreateParams { Model = "claude-opus-5", MaxTokens = 4096, Betas = ["code-execution-2025-08-25", "skills-2025-10-02"], Container = new BetaContainerParams { Skills = [ new BetaSkillParams { Type = BetaSkillParamsType.Custom, SkillID = "skill_01AbCdEfGhIjKlMnOpQrStUv", Version = "latest", }, ], }, Messages = [new() { Role = Role.User, Content = "Process data" }], Tools = [new BetaCodeExecutionTool20250825()], }; var response = await client.Beta.Messages.Create(parameters); Console.WriteLine(response); } catch (AnthropicBadRequestException e) when (e.Message.Contains("skill")) { Console.WriteLine($"Skill error: {e.Message}"); } ``` ```go Go client := anthropic.NewClient() response, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{ Model: "claude-opus-5", MaxTokens: 4096, Betas: []anthropic.AnthropicBeta{"code-execution-2025-08-25", anthropic.AnthropicBetaSkills2025_10_02}, Container: anthropic.BetaMessageNewParamsContainerUnion{ OfContainers: &anthropic.BetaContainerParams{ Skills: []anthropic.BetaSkillParams{ { Type: anthropic.BetaSkillParamsTypeCustom, SkillID: "skill_01AbCdEfGhIjKlMnOpQrStUv", Version: anthropic.String("latest"), }, }, }, }, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Process data")), }, Tools: []anthropic.BetaToolUnionParam{ {OfCodeExecutionTool20250825: &anthropic.BetaCodeExecutionTool20250825Param{}}, }, }) if err != nil { var apierr *anthropic.Error if errors.As(err, &apierr) && apierr.Type() == anthropic.ErrorTypeInvalidRequestError && strings.Contains(apierr.Error(), "skill") { fmt.Printf("Skill error: %v\n", apierr) } else { log.Fatal(err) } return } fmt.Println(response) ``` ```java Java import com.anthropic.errors.BadRequestException; import com.anthropic.models.beta.messages.BetaContainerParams; import com.anthropic.models.beta.messages.BetaSkillParams; import com.anthropic.models.beta.messages.BetaCodeExecutionTool20250825; // ... void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); try { MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(4096L) .addBeta("code-execution-2025-08-25") .addBeta("skills-2025-10-02") .container(BetaContainerParams.builder() .addSkill(BetaSkillParams.builder() .type(BetaSkillParams.Type.CUSTOM) .skillId("skill_01AbCdEfGhIjKlMnOpQrStUv") .version("latest") .build()) .build()) .addUserMessage("Process data") .addTool(BetaCodeExecutionTool20250825.builder().build()) .build(); BetaMessage response = client.beta().messages().create(params); System.out.println(response); } catch (BadRequestException e) { if (e.getMessage().contains("skill")) { System.err.println("Skill error: " + e.getMessage()); } else { throw e; } } } ``` ```php PHP use Anthropic\Core\Exceptions\BadRequestException; $client = new Client(); try { $message = $client->beta->messages->create( maxTokens: 4096, messages: [ ['role' => 'user', 'content' => 'Process data'] ], model: 'claude-opus-5', betas: ['code-execution-2025-08-25', 'skills-2025-10-02'], container: [ 'skills' => [ [ 'type' => 'custom', 'skill_id' => 'skill_01AbCdEfGhIjKlMnOpQrStUv', 'version' => 'latest' ] ] ], tools: [ ['type' => 'code_execution_20250825', 'name' => 'code_execution'] ] ); echo $message; } catch (BadRequestException $e) { if (str_contains($e->getMessage(), 'skill')) { echo "Skill error: " . $e->getMessage(); } else { throw $e; } } ``` ```ruby Ruby client = Anthropic::Client.new begin response = client.beta.messages.create( model: "claude-opus-5", max_tokens: 4096, betas: ["code-execution-2025-08-25", "skills-2025-10-02"], container: { skills: [ { type: "custom", skill_id: "skill_01AbCdEfGhIjKlMnOpQrStUv", version: "latest" } ] }, messages: [{ role: "user", content: "Process data" }], tools: [{ type: "code_execution_20250825", name: "code_execution" }] ) rescue Anthropic::Errors::BadRequestError => e if e.message.include?("skill") puts "Skill error: #{e.message}" else raise end end ``` *** ## Data retention Agent Skills are not covered by ZDR arrangements. Skill definitions and execution data are retained according to Anthropic's standard data retention policy. For ZDR eligibility across all features, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention). ## Next steps Complete API reference with all endpoints Learn how to write effective Skills that Claude can discover and use successfully. Run Python and bash code in a sandboxed container to analyze data, generate files, and iterate on solutions. ### MCP --- title: MCP connector url: https://platform.claude.com/docs/en/agents-and-tools/mcp-connector description: Connect to remote MCP servers directly from the Messages API without an MCP client, and allowlist, denylist, or configure individual tools. --- ## Compatibility - Status: Beta - [Beta header](https://platform.claude.com/docs/en/api/beta-headers): `mcp-client-2025-11-20` - [ZDR](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention): not eligible - Platforms: Claude API (beta), Claude Platform on AWS (beta), Microsoft Foundry (beta) [1]; not available on Amazon Bedrock, Google Cloud 1. On [Microsoft Foundry](https://platform.claude.com/docs/en/build-with-claude/claude-in-microsoft-foundry), the MCP connector requires a [Hosted on Anthropic deployment](https://platform.claude.com/docs/en/build-with-claude/claude-in-microsoft-foundry#additional-features-not-supported-when-hosted-on-azure). Claude's Model Context Protocol (MCP) connector feature enables you to connect to remote MCP servers directly from the Messages API without a separate MCP client. The previous version of this feature (`mcp-client-2025-04-04`) is deprecated. See [Deprecated version: mcp-client-2025-04-04](https://platform.claude.com/docs/en/agents-and-tools/mcp-connector#deprecated-version-mcp-client-2025-04-04). ## Key features * **Direct API integration:** Connect to MCP servers without implementing an MCP client * **Tool calling support:** Access MCP tools through the Messages API * **Flexible tool configuration:** Enable all tools, allowlist specific tools, or denylist unwanted tools * **Per-tool configuration:** Configure individual tools with custom settings * **OAuth authentication:** Support for OAuth Bearer tokens for authenticated servers * **Multiple servers:** Connect to multiple MCP servers in a single request ## When Claude uses MCP tools Once an MCP server is connected, Claude calls its tools when the user's request maps to a tool's described capability, either explicitly ("search Jira for open bugs") or implicitly ("what's blocking the release?" with a Jira server attached). Claude does **not** call an MCP tool for general knowledge questions about a connected service. Asking "how do Notion databases work?" with a Notion server attached is answered directly; asking "what's in my Projects database?" triggers the tool. You can steer how readily Claude calls MCP tools through your system prompt. See [When Claude uses tools](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview#when-claude-uses-tools) for general guidance and example phrasings. ## Limitations * Of the feature set of the [MCP specification](https://modelcontextprotocol.io/introduction#explore-mcp), only [tool calls](https://modelcontextprotocol.io/docs/concepts/tools) are currently supported. * The server must be publicly exposed through HTTP (supports both Streamable HTTP and SSE transports). Local STDIO servers cannot be connected directly. ## Using the MCP connector in the Messages API The MCP connector uses two components: 1. **MCP server definition** (`mcp_servers` array): Defines server connection details (URL, authentication) 2. **MCP toolset** (`tools` array): Configures which tools to enable and how to configure them ### Basic example This example enables all tools from an MCP server with default configuration: ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "Content-Type: application/json" \ -H "X-API-Key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: mcp-client-2025-11-20" \ -d '{ "model": "claude-opus-5", "max_tokens": 1000, "messages": [{"role": "user", "content": "What tools do you have available?"}], "mcp_servers": [ { "type": "url", "url": "https://example-server.modelcontextprotocol.io/sse", "name": "example-mcp", "authorization_token": "YOUR_TOKEN" } ], "tools": [ { "type": "mcp_toolset", "mcp_server_name": "example-mcp" } ] }' ``` ```bash CLI ant beta:messages create --beta mcp-client-2025-11-20 <<'YAML' model: claude-opus-5 max_tokens: 1000 messages: - role: user content: What tools do you have available? mcp_servers: - type: url url: https://example-server.modelcontextprotocol.io/sse name: example-mcp authorization_token: YOUR_TOKEN tools: - type: mcp_toolset mcp_server_name: example-mcp YAML ``` ```python Python client = anthropic.Anthropic() response = client.beta.messages.create( model="claude-opus-5", max_tokens=1000, messages=[{"role": "user", "content": "What tools do you have available?"}], mcp_servers=[ { "type": "url", "url": "https://example-server.modelcontextprotocol.io/sse", "name": "example-mcp", "authorization_token": "YOUR_TOKEN", } ], tools=[{"type": "mcp_toolset", "mcp_server_name": "example-mcp"}], betas=["mcp-client-2025-11-20"], ) print(response) ``` ```typescript TypeScript const anthropic = new Anthropic(); const response = await anthropic.beta.messages.create({ model: "claude-opus-5", max_tokens: 1000, messages: [ { role: "user", content: "What tools do you have available?" } ], mcp_servers: [ { type: "url", url: "https://example-server.modelcontextprotocol.io/sse", name: "example-mcp", authorization_token: "YOUR_TOKEN" } ], tools: [ { type: "mcp_toolset", mcp_server_name: "example-mcp" } ], betas: ["mcp-client-2025-11-20"] }); console.log(response); ``` ```csharp C# AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1000, Messages = new List { new() { Role = Role.User, Content = "What tools do you have available?" } }, McpServers = new List { new() { Url = "https://example-server.modelcontextprotocol.io/sse", Name = "example-mcp", AuthorizationToken = "YOUR_TOKEN" } }, Tools = new List { new BetaMcpToolset("example-mcp") }, Betas = [AnthropicBeta.McpClient2025_11_20] }; var message = await client.Beta.Messages.Create(parameters); Console.WriteLine(message); ``` ```go Go client := anthropic.NewClient() response, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1000, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("What tools do you have available?")), }, MCPServers: []anthropic.BetaRequestMCPServerURLDefinitionParam{ { URL: "https://example-server.modelcontextprotocol.io/sse", Name: "example-mcp", AuthorizationToken: anthropic.String("YOUR_TOKEN"), }, }, Tools: []anthropic.BetaToolUnionParam{ {OfMCPToolset: &anthropic.BetaMCPToolsetParam{ MCPServerName: "example-mcp", }}, }, Betas: []anthropic.AnthropicBeta{ anthropic.AnthropicBetaMCPClient2025_11_20, }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java import com.anthropic.models.beta.messages.BetaMcpToolset; // ... import com.anthropic.models.beta.messages.BetaRequestMcpServerUrlDefinition; // ... void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1000L) .addUserMessage("What tools do you have available?") .addMcpServer(BetaRequestMcpServerUrlDefinition.builder() .url("https://example-server.modelcontextprotocol.io/sse") .name("example-mcp") .authorizationToken("YOUR_TOKEN") .build()) .addTool(BetaMcpToolset.builder() .mcpServerName("example-mcp") .build()) .addBeta(AnthropicBeta.MCP_CLIENT_2025_11_20) .build(); BetaMessage response = client.beta().messages().create(params); IO.println(response); } ``` ```php PHP $client = new Client(); $message = $client->beta->messages->create( maxTokens: 1000, messages: [ ['role' => 'user', 'content' => 'What tools do you have available?'] ], model: 'claude-opus-5', mcpServers: [ [ 'type' => 'url', 'url' => 'https://example-server.modelcontextprotocol.io/sse', 'name' => 'example-mcp', 'authorization_token' => 'YOUR_TOKEN', ], ], tools: [ [ 'type' => 'mcp_toolset', 'mcp_server_name' => 'example-mcp', ], ], betas: ['mcp-client-2025-11-20'], ); echo $message; ``` ```ruby Ruby client = Anthropic::Client.new response = client.beta.messages.create( model: "claude-opus-5", max_tokens: 1000, messages: [ { role: "user", content: "What tools do you have available?" } ], mcp_servers: [ { type: "url", url: "https://example-server.modelcontextprotocol.io/sse", name: "example-mcp", authorization_token: "YOUR_TOKEN" } ], tools: [ { type: "mcp_toolset", mcp_server_name: "example-mcp" } ], betas: ["mcp-client-2025-11-20"] ) puts response ``` ## MCP server configuration Each MCP server in the `mcp_servers` array defines the connection details: ```json { "type": "url", "url": "https://example-server.modelcontextprotocol.io/sse", "name": "example-mcp", "authorization_token": "YOUR_TOKEN" } ``` ### Field descriptions | Property | Type | Required | Description | | --------------------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `type` | string | Yes | Currently only "url" is supported. | | `url` | string | Yes | The URL of the MCP server. Must start with https\://. | | `name` | string | Yes | A unique identifier for this MCP server. Must be referenced by exactly one MCPToolset in the `tools` array. | | `authorization_token` | string | No | OAuth authorization token if required by the MCP server. See [Authentication](https://platform.claude.com/docs/en/agents-and-tools/mcp-connector#authentication) for how to obtain one, or the [MCP specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization) for protocol details. | ## MCP toolset configuration The MCPToolset lives in the `tools` array and configures which tools from the MCP server are enabled and how they should be configured. ### Basic structure ```json { "type": "mcp_toolset", "mcp_server_name": "example-mcp", "default_config": { "enabled": true, "defer_loading": false }, "configs": { "specific_tool_name": { "enabled": true, "defer_loading": true } } } ``` ### Field descriptions | Property | Type | Required | Description | | ----------------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `type` | string | Yes | Must be "mcp\_toolset". | | `mcp_server_name` | string | Yes | Must match a server name defined in the `mcp_servers` array. | | `default_config` | object | No | Default configuration applied to all tools in this set. Individual tool configs in `configs` override these defaults. | | `configs` | object | No | Per-tool configuration overrides. Keys are tool names, values are configuration objects. | | `cache_control` | object | No | [Prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) cache breakpoint configuration for this toolset. | ### Tool configuration options Each tool (whether configured in `default_config` or in `configs`) supports the following fields: | Property | Type | Default | Description | | --------------- | ------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `enabled` | boolean | `true` | Whether this tool is enabled. | | `defer_loading` | boolean | `false` | If true, tool description is not sent to the model initially. Used with [Tool search tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool). | For the full directory of Anthropic-provided tools and optional properties such as `defer_loading`, see the [Tool reference](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-reference). For searching across large tool sets, see [Tool search tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool). ### Configuration merging Configuration values merge with this precedence (highest to lowest): 1. Tool-specific settings in `configs` 2. Set-level `default_config` 3. System defaults Example: ```json { "type": "mcp_toolset", "mcp_server_name": "google-calendar-mcp", "default_config": { "defer_loading": true }, "configs": { "search_events": { "enabled": false } } } ``` Results in: * `search_events`: `enabled: false` (from configs), `defer_loading: true` (from default\_config) * All other tools: `enabled: true` (system default), `defer_loading: true` (from default\_config) ## Common configuration patterns ### Enable all tools with default configuration The simplest pattern: enable all tools from a server: ```json { "type": "mcp_toolset", "mcp_server_name": "google-calendar-mcp" } ``` ### Allowlist: enable only specific tools Set `enabled: false` as the default, then explicitly enable specific tools: ```json { "type": "mcp_toolset", "mcp_server_name": "google-calendar-mcp", "default_config": { "enabled": false }, "configs": { "search_events": { "enabled": true }, "create_event": { "enabled": true } } } ``` ### Denylist: disable specific tools Enable all tools by default, then explicitly disable unwanted tools. Denylisting write or destructive tools is recommended when building read-only assistants, or when you want a human confirmation step before state changes: ```json { "type": "mcp_toolset", "mcp_server_name": "google-calendar-mcp", "configs": { "delete_all_events": { "enabled": false }, "share_calendar_publicly": { "enabled": false } } } ``` ### Mixed: allowlist with per-tool configuration Combine allowlisting with custom configuration for each tool: ```json { "type": "mcp_toolset", "mcp_server_name": "google-calendar-mcp", "default_config": { "enabled": false, "defer_loading": true }, "configs": { "search_events": { "enabled": true, "defer_loading": false }, "list_events": { "enabled": true } } } ``` In this example: * `search_events` is enabled with `defer_loading: false` * `list_events` is enabled with `defer_loading: true` (inherited from default\_config) * All other tools are disabled ## Validation rules The API enforces these validation rules: * **Server must exist:** The `mcp_server_name` in an MCPToolset must match a server defined in the `mcp_servers` array * **Server must be used:** Every MCP server defined in `mcp_servers` must be referenced by exactly one MCPToolset * **Unique toolset per server:** Each MCP server can only be referenced by one MCPToolset * **Unknown tool names:** If a tool name in `configs` doesn't exist on the MCP server, a backend warning is logged but no error is returned (MCP servers may have dynamic tool availability) ## Response content types When Claude uses MCP tools, the response includes two new content block types: ### MCP tool use block ```json { "type": "mcp_tool_use", "id": "mcptoolu_014Q35RayjACSWkSj4X2yov1", "name": "echo", "server_name": "example-mcp", "input": { "param1": "value1", "param2": "value2" } } ``` ### MCP tool result block ```json { "type": "mcp_tool_result", "tool_use_id": "mcptoolu_014Q35RayjACSWkSj4X2yov1", "is_error": false, "content": [ { "type": "text", "text": "Hello" } ] } ``` ## Multiple MCP servers You can connect to multiple MCP servers by including multiple server definitions in `mcp_servers` and a corresponding MCPToolset for each in the `tools` array: ```json { "model": "claude-opus-5", "max_tokens": 1000, "messages": [ { "role": "user", "content": "Use tools from both mcp-server-1 and mcp-server-2 to complete this task" } ], "mcp_servers": [ { "type": "url", "url": "https://mcp.example1.com/sse", "name": "mcp-server-1", "authorization_token": "TOKEN1" }, { "type": "url", "url": "https://mcp.example2.com/sse", "name": "mcp-server-2", "authorization_token": "TOKEN2" } ], "tools": [ { "type": "mcp_toolset", "mcp_server_name": "mcp-server-1" }, { "type": "mcp_toolset", "mcp_server_name": "mcp-server-2", "default_config": { "defer_loading": true } } ] } ``` With many tools available, Claude selects based on tool names and descriptions. Clear, specific tool descriptions improve selection accuracy. For large tool sets (dozens of tools across several servers), consider enabling [`defer_loading`](https://platform.claude.com/docs/en/agents-and-tools/mcp-connector#tool-configuration-options) with the [Tool search tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool) so only relevant tools are surfaced per query. ## Authentication For MCP servers that require OAuth authentication, you'll need to obtain an access token. The MCP connector beta supports passing an `authorization_token` parameter in the MCP server definition. API consumers are expected to handle the OAuth flow and obtain the access token prior to making the API call, and to refresh the token as needed. ### Obtaining an access token for testing The MCP inspector can guide you through the process of obtaining an access token for testing purposes. 1. Run the inspector with the following command. You need Node.js installed on your machine. ```bash npx @modelcontextprotocol/inspector ``` 2. In the sidebar on the left, for **Transport type**, select either **SSE** or **Streamable HTTP**. 3. Enter the URL of the MCP server. 4. In the right area, click **Open Auth Settings** after **Need to configure authentication?**. 5. Click **Quick OAuth Flow** and authorize on the OAuth screen. 6. Follow the steps in the **OAuth Flow Progress** section of the inspector and click **Continue** until you reach **Authentication complete**. 7. Copy the `access_token` value. 8. Paste it into the `authorization_token` field in your MCP server configuration. ### Using the access token Once you've obtained an access token using either of the preceding OAuth flows, you can use it in your MCP server configuration: ```json { "mcp_servers": [ { "type": "url", "url": "https://example-server.modelcontextprotocol.io/sse", "name": "authenticated-server", "authorization_token": "YOUR_ACCESS_TOKEN_HERE" } ] } ``` For detailed explanations of the OAuth flow, refer to the [Authorization section](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization) in the MCP specification. ## Client-side MCP helpers If you manage your own MCP client connection (for example, with local stdio servers, MCP prompts, or MCP resources), the SDKs provide helper functions that convert between MCP types and Claude API types. This eliminates manual conversion code when using an MCP SDK for your language (for example, the [TypeScript MCP SDK](https://github.com/modelcontextprotocol/typescript-sdk)) alongside the Anthropic SDK. Use the [`mcp_servers` API parameter](https://platform.claude.com/docs/en/agents-and-tools/mcp-connector#using-the-mcp-connector-in-the-messages-api) when you have remote servers accessible by URL and only need tool support. Use the client-side helpers when you need local servers, prompts, resources, or more control over the connection with the base SDK. ### Installation Install both the Anthropic SDK and the MCP SDK: The MCP helpers are included in the `mcp` extra, which requires Python 3.10 or later: ```bash pip install "anthropic[mcp]" ``` ```bash npm install @anthropic-ai/sdk @modelcontextprotocol/sdk ``` The helpers live in the separate `Anthropic.Mcp` package; the MCP client itself comes from the official [ModelContextProtocol package](https://www.nuget.org/packages/ModelContextProtocol): ```bash dotnet add package Anthropic.Mcp dotnet add package ModelContextProtocol ``` The helpers live in the `mcp` subpackage of the Go SDK, which builds on the [MCP Go SDK](https://github.com/modelcontextprotocol/go-sdk): ```bash go get github.com/anthropics/anthropic-sdk-go/mcp ``` The helpers live in the separate `anthropic-java-mcp` artifact, which requires Java 17 or later (the core SDK supports Java 8): ```kotlin implementation("com.anthropic:anthropic-java-mcp:2.53.0") ``` ```xml com.anthropic anthropic-java-mcp 2.53.0 ``` The helpers use the official [MCP PHP SDK](https://packagist.org/packages/mcp/sdk): ```bash composer require "anthropic-ai/sdk" "guzzlehttp/guzzle:^7" "mcp/sdk" ``` The helpers use the official [`mcp` gem](https://rubygems.org/gems/mcp): ```bash bundle add anthropic mcp ``` ### Available helpers Import the helpers for your language: ```python Python from anthropic.lib.tools.mcp import ( async_mcp_tool, mcp_message, mcp_resource_to_content, mcp_resource_to_file, ) ``` ```typescript TypeScript import { mcpTools, mcpMessages, mcpResourceToContent, mcpResourceToFile } from "@anthropic-ai/sdk/helpers/beta/mcp"; ``` ```csharp C# using Anthropic.Helpers.Beta; using Anthropic.Helpers.Beta.Mcp; ``` ```go Go import ( "github.com/anthropics/anthropic-sdk-go/mcp" ) ``` ```java Java import com.anthropic.helpers.McpBetaTool; import com.anthropic.mcp.BetaMcp; ``` ```php PHP use Anthropic\Lib\Tools\BetaMcp; ``` ```ruby Ruby require "anthropic" # The helpers are exposed on the Anthropic::Mcp module ``` Helper names and exact signatures follow each language's conventions; this table shows the TypeScript forms: | Helper | Description | | -------------------------------- | --------------------------------------------------------------------------------------- | | `mcpTools(tools, mcpClient)` | Converts MCP tools to Claude API tools for use with `client.beta.messages.toolRunner()` | | `mcpMessages(messages)` | Converts MCP prompt messages to Claude API message format | | `mcpResourceToContent(resource)` | Converts an MCP resource to a Claude API content block | | `mcpResourceToFile(resource)` | Converts an MCP resource to a file object for upload | ### Use MCP tools Convert MCP tools for use with the SDK's [tool runner](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-runner), which handles tool execution automatically: ```python Python from anthropic.lib.tools.mcp import async_mcp_tool from mcp import ClientSession from mcp.client.stdio import StdioServerParameters, stdio_client client = AsyncAnthropic() async def main() -> None: # Connect to an MCP server server_params = StdioServerParameters(command="mcp-server") async with stdio_client(server_params) as (read, write): async with ClientSession(read, write) as mcp_client: await mcp_client.initialize() # List tools and convert them for the Claude API tools_result = await mcp_client.list_tools() runner = client.beta.messages.tool_runner( model="claude-opus-5", max_tokens=1024, messages=[ {"role": "user", "content": "What tools do you have available?"}, ], tools=[async_mcp_tool(tool, mcp_client) for tool in tools_result.tools], ) final_message = await runner.until_done() print(final_message) asyncio.run(main()) ``` ```typescript TypeScript import { mcpTools, type MCPCallToolResultLike, type MCPClientLike } from "@anthropic-ai/sdk/helpers/beta/mcp"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; const anthropic = new Anthropic(); // Connect to an MCP server const transport = new StdioClientTransport({ command: "mcp-server", args: [] }); const mcpClient = new Client({ name: "my-client", version: "1.0.0" }); await mcpClient.connect(transport); // List tools and convert them for the Claude API const { tools } = await mcpClient.listTools(); // The MCP SDK's callTool return type still includes a legacy result shape that // mcpTools does not accept; narrow it. Drop this once MCPClientLike widens. const mcpClientForTools: MCPClientLike = { callTool: (params) => mcpClient.callTool(params) as Promise }; const finalMessage = await anthropic.beta.messages.toolRunner({ model: "claude-opus-5", max_tokens: 1024, messages: [{ role: "user", content: "What tools do you have available?" }], tools: mcpTools(tools, mcpClientForTools) }); console.log(finalMessage); ``` ```csharp C# using Anthropic.Helpers.Beta; using Anthropic.Helpers.Beta.Mcp; using Anthropic.Models.Beta.Messages; using ModelContextProtocol.Client; using Messages = Anthropic.Models.Messages; var anthropic = new AnthropicClient(); // Connect to an MCP server await using var mcpClient = await McpClient.CreateAsync( new StdioClientTransport(new StdioClientTransportOptions { Command = "mcp-server" }) ); // List tools and convert them for the Claude API var tools = await BetaMcp.ListToolsAsync(mcpClient); var runner = anthropic.Beta.Messages.ToolRunner( new MessageCreateParams { Model = Messages::Model.ClaudeOpus5, MaxTokens = 1024, Messages = [ new BetaMessageParam { Role = Role.User, Content = "What tools do you have available?", }, ], }, tools ); var finalMessage = await runner.RunUntilDoneAsync(); Console.WriteLine(finalMessage); ``` ```go Go import ( // ... // ... "github.com/anthropics/anthropic-sdk-go/mcp" mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" ) func main() { client := anthropic.NewClient() ctx := context.Background() // Connect to an MCP server mcpClient := mcpsdk.NewClient(&mcpsdk.Implementation{Name: "my-client", Version: "1.0.0"}, nil) session, err := mcpClient.Connect(ctx, &mcpsdk.CommandTransport{Command: exec.Command("mcp-server")}, nil) if err != nil { log.Fatal(err) } defer session.Close() // List tools and convert them for the Claude API toolsResult, err := session.ListTools(ctx, nil) if err != nil { log.Fatal(err) } betaTools, err := mcp.NewBetaTools(toolsResult.Tools, session) if err != nil { log.Fatal(err) } runner := client.Beta.Messages.NewToolRunner(betaTools, anthropic.BetaToolRunnerParams{ BetaMessageNewParams: anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("What tools do you have available?")), }, }, }) finalMessage, err := runner.RunToCompletion(ctx) if err != nil { log.Fatal(err) } fmt.Println(finalMessage.RawJSON()) } ``` ```java Java import com.anthropic.helpers.BetaToolRunner; import com.anthropic.helpers.McpBetaTool; import com.anthropic.mcp.BetaMcp; import com.anthropic.models.beta.messages.BetaMessage; import com.anthropic.models.beta.messages.MessageCreateParams; import com.anthropic.models.messages.Model; import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.McpSyncClient; import io.modelcontextprotocol.client.transport.ServerParameters; import io.modelcontextprotocol.client.transport.StdioClientTransport; import io.modelcontextprotocol.json.McpJsonDefaults; import io.modelcontextprotocol.spec.McpSchema; // ... void main() throws Exception { AnthropicClient anthropic = AnthropicOkHttpClient.fromEnv(); // Connect to an MCP server StdioClientTransport transport = new StdioClientTransport( ServerParameters.builder("mcp-server").build(), McpJsonDefaults.getMapper()); try (McpSyncClient mcpClient = McpClient.sync(transport) .clientInfo(new McpSchema.Implementation("my-client", "1.0.0")) .build()) { mcpClient.initialize(); // List tools and convert them for the Claude API List betaTools = BetaMcp.mcpTools(mcpClient.listTools().tools(), mcpClient); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addUserMessage("What tools do you have available?") .addTools(betaTools) .build(); // The runner yields one message per assistant turn; the last is the final response BetaToolRunner runner = anthropic.beta().messages().toolRunner(params); BetaMessage finalMessage = null; for (BetaMessage message : runner) { finalMessage = message; } IO.println(finalMessage); } } ``` ```php PHP use Anthropic\Lib\Tools\BetaMcp; use Mcp\Client; use Mcp\Client\Transport\HttpTransport; $anthropic = new Anthropic(); // Connect to an MCP server. The PHP MCP client connects over HTTP; point this // at your server's endpoint. $mcp = Client::builder()->build(); $mcp->connect(new HttpTransport('http://localhost:8000/mcp')); // List tools and convert them for the Claude API $runner = $anthropic->beta->messages->toolRunner( maxTokens: 1024, messages: [['role' => 'user', 'content' => 'What tools do you have available?']], model: 'claude-opus-5', tools: BetaMcp::tools($mcp->listTools()->tools, $mcp), ); echo $runner->runUntilDone(), "\n"; ``` ```ruby Ruby require "mcp" anthropic = Anthropic::Client.new # Connect to an MCP server transport = MCP::Client::Stdio.new(command: "mcp-server") mcp_client = MCP::Client.new(transport: transport) mcp_client.connect # List tools and convert them for the Claude API runner = anthropic.beta.messages.tool_runner( model: "claude-opus-5", max_tokens: 1024, messages: [{ role: "user", content: "What tools do you have available?" }], tools: Anthropic::Mcp.tools(mcp_client.tools, mcp_client) ) final_message = runner.run_until_finished.last puts final_message ``` ### Use MCP prompts Convert MCP prompt messages into Claude API message format: ```python Python from anthropic.lib.tools.mcp import mcp_message prompt = await mcp_client.get_prompt(name="my-prompt") response = await client.beta.messages.create( model="claude-opus-5", max_tokens=1024, messages=[mcp_message(message) for message in prompt.messages], ) print(response) ``` ```typescript TypeScript import { mcpMessages } from "@anthropic-ai/sdk/helpers/beta/mcp"; const { messages } = await mcpClient.getPrompt({ name: "my-prompt" }); const response = await anthropic.beta.messages.create({ model: "claude-opus-5", max_tokens: 1024, messages: mcpMessages(messages) }); console.log(response); ``` ```csharp C# var prompt = await mcpClient.GetPromptAsync("my-prompt"); var response = await anthropic.Beta.Messages.Create( new MessageCreateParams { Model = Messages::Model.ClaudeOpus5, MaxTokens = 1024, Messages = BetaMcp.Messages(prompt.Messages), } ); Console.WriteLine(response); ``` ```go Go prompt, err := session.GetPrompt(ctx, &mcpsdk.GetPromptParams{Name: "my-prompt"}) if err != nil { log.Fatal(err) } messages := make([]anthropic.BetaMessageParam, 0, len(prompt.Messages)) for _, promptMessage := range prompt.Messages { message, err := mcp.ToMessage(promptMessage) if err != nil { log.Fatal(err) } messages = append(messages, message) } response, err := client.Beta.Messages.New(ctx, anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: messages, }) if err != nil { log.Fatal(err) } fmt.Println(response.RawJSON()) ``` ```java Java McpSchema.GetPromptResult prompt = mcpClient.getPrompt( new McpSchema.GetPromptRequest("my-prompt", Map.of())); BetaMessage response = anthropic.beta().messages().create(MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .messages(BetaMcp.mcpMessages(prompt.messages())) .build()); IO.println(response); ``` ```php PHP $prompt = $mcp->getPrompt('my-prompt'); $response = $anthropic->beta->messages->create( maxTokens: 1024, messages: array_map(BetaMcp::message(...), $prompt->messages), model: 'claude-opus-5', ); echo $response, "\n"; ``` ```ruby Ruby prompt = mcp_client.get_prompt(name: "my-prompt") response = anthropic.beta.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: prompt["messages"].map { |message| Anthropic::Mcp.message(message) } ) puts response ``` ### Use MCP resources Convert MCP resources into content blocks to include in messages, or into file objects for upload: ```python Python from anthropic.lib.tools.mcp import ( mcp_resource_to_content, mcp_resource_to_file, ) # As a content block in a message resource = await mcp_client.read_resource(uri="file:///path/to/doc.txt") response = await client.beta.messages.create( model="claude-opus-5", max_tokens=1024, messages=[ { "role": "user", "content": [ mcp_resource_to_content(resource), {"type": "text", "text": "Summarize this document"}, ], } ], ) print(response) # As a file upload file_resource = await mcp_client.read_resource( uri="file:///path/to/data.json", ) uploaded = await client.beta.files.upload( file=mcp_resource_to_file(file_resource), ) print(uploaded.id) ``` ```typescript TypeScript import { mcpResourceToContent, mcpResourceToFile } from "@anthropic-ai/sdk/helpers/beta/mcp"; // As a content block in a message const resource = await mcpClient.readResource({ uri: "file:///path/to/doc.txt" }); const response = await anthropic.beta.messages.create({ model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: [ mcpResourceToContent(resource), { type: "text", text: "Summarize this document" } ] } ] }); console.log(response); // As a file upload const fileResource = await mcpClient.readResource({ uri: "file:///path/to/data.json" }); const uploaded = await anthropic.beta.files.upload({ file: mcpResourceToFile(fileResource) }); console.log(uploaded.id); ``` ```csharp C# // As a content block in a message var resource = await mcpClient.ReadResourceAsync("file:///path/to/doc.txt"); var response = await anthropic.Beta.Messages.Create( new MessageCreateParams { Model = Messages::Model.ClaudeOpus5, MaxTokens = 1024, Messages = [ new BetaMessageParam { Role = Role.User, Content = new BetaMessageParamContent( [ BetaMcp.ResourceToContent(resource), new BetaTextBlockParam { Text = "Summarize this document" }, ] ), }, ], } ); Console.WriteLine(response); // As a file upload var fileResource = await mcpClient.ReadResourceAsync("file:///path/to/data.json"); var (filename, data, mediaType) = BetaMcp.ResourceToFile(fileResource); // Build the file part explicitly so the resource's filename and MIME type // carry through to the upload. var file = new BinaryContent { Stream = new MemoryStream(data), FileName = filename }; if (mediaType is not null) { file.ContentType = new(mediaType); } var uploaded = await anthropic.Beta.Files.Upload(new FileUploadParams { File = file }); Console.WriteLine(uploaded.ID); ``` ```go Go // As a content block in a message resource, err := session.ReadResource(ctx, &mcpsdk.ReadResourceParams{URI: "file:///path/to/doc.txt"}) if err != nil { log.Fatal(err) } block, err := mcp.ResourceToBlock(resource) if err != nil { log.Fatal(err) } response, err := client.Beta.Messages.New(ctx, anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage( // ResourceToBlock returns the tool-result content union; message // content is a separate union type, so re-wrap the shared variants // (mcp.ToMessage does the same internally). anthropic.BetaContentBlockParamUnion{ OfText: block.OfText, OfImage: block.OfImage, OfDocument: block.OfDocument, }, anthropic.NewBetaTextBlock("Summarize this document"), ), }, }) if err != nil { log.Fatal(err) } fmt.Println(response.RawJSON()) // As a file upload fileResult, err := session.ReadResource(ctx, &mcpsdk.ReadResourceParams{URI: "file:///path/to/data.json"}) if err != nil { log.Fatal(err) } fileReader, err := mcp.ResourceToFile(fileResult) if err != nil { log.Fatal(err) } uploaded, err := client.Beta.Files.Upload(ctx, anthropic.BetaFileUploadParams{File: fileReader}) if err != nil { log.Fatal(err) } fmt.Println(uploaded.ID) ``` ```java Java // As a content block in a message McpSchema.ReadResourceResult resource = mcpClient.readResource( new McpSchema.ReadResourceRequest("file:///path/to/doc.txt")); List content = new ArrayList<>(BetaMcp.mcpResourceContents(resource)); content.add(BetaContentBlockParam.ofText( BetaTextBlockParam.builder().text("Summarize this document").build())); BetaMessage response = anthropic.beta().messages().create(MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addUserMessageOfBetaContentBlockParams(content) .build()); IO.println(response); // As a file upload McpSchema.ReadResourceResult fileResource = mcpClient.readResource( new McpSchema.ReadResourceRequest("file:///path/to/data.json")); McpResourceFile resourceFile = BetaMcp.mcpResourceFiles(fileResource).getFirst(); // Build the file part explicitly so the resource's filename and MIME type // carry through to the upload. MultipartField.Builder fileField = MultipartField.builder() .value(new ByteArrayInputStream(resourceFile.content())) .filename(resourceFile.filename()); if (resourceFile.mimeType() != null) { fileField.contentType(resourceFile.mimeType()); } var uploaded = anthropic.beta().files().upload(FileUploadParams.builder() .file(fileField.build()) .build()); IO.println(uploaded.id()); ``` ```php PHP // As a content block in a message $resource = $mcp->readResource('file:///path/to/doc.txt'); $response = $anthropic->beta->messages->create( maxTokens: 1024, messages: [ [ 'role' => 'user', 'content' => [ BetaMcp::resourceToContent($resource), ['type' => 'text', 'text' => 'Summarize this document'], ], ], ], model: 'claude-opus-5', ); echo $response, "\n"; // As a file upload $fileResource = $mcp->readResource('file:///path/to/data.json'); $file = $anthropic->beta->files->upload(file: BetaMcp::resourceToFile($fileResource)); echo $file->id, "\n"; ``` ```ruby Ruby # As a content block in a message resource = mcp_client.read_resource(uri: "file:///path/to/doc.txt") response = anthropic.beta.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [ { role: "user", content: [ *Anthropic::Mcp.resource_to_contents(resource), { type: "text", text: "Summarize this document" } ] } ] ) puts response # As a file upload file_resource = mcp_client.read_resource(uri: "file:///path/to/data.json") file = Anthropic::Mcp.resource_to_files(file_resource).first uploaded_file = anthropic.beta.files.upload(file: file) puts uploaded_file.id ``` ### Error handling The conversion functions throw `UnsupportedMCPValueError` if an MCP value isn't supported by the Claude API (in Go, the helpers return an `UnsupportedValueError`; in Java and C#, they throw `AnthropicInvalidDataException`). This can happen with unsupported content types, MIME types, or resource links (resolve resource links with your MCP client before converting). ## Batch requests You can include `mcp_servers` in [Message Batches API](https://platform.claude.com/docs/en/build-with-claude/batch-processing) requests. MCP tool calls through the Batches API are priced the same as those in regular Messages API requests. ## Data retention The MCP connector is not covered by ZDR arrangements. Data exchanged with MCP servers, including tool definitions and execution results, is retained according to Anthropic's standard data retention policy. For ZDR eligibility across all features, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention). ## Migration guide If you're using the deprecated `mcp-client-2025-04-04` beta header, follow this guide to migrate to the new version. ### Key changes 1. **New beta header:** Change from `mcp-client-2025-04-04` to `mcp-client-2025-11-20` 2. **Tool configuration moved:** Tool configuration now lives in the `tools` array as MCPToolset objects, not in the MCP server definition 3. **More flexible configuration:** New pattern supports allowlisting, denylisting, and per-tool configuration ### Migration steps **Before (deprecated):** ```json { "model": "claude-opus-5", "max_tokens": 1000, "messages": [ // ... ], "mcp_servers": [ { "type": "url", "url": "https://mcp.example.com/sse", "name": "example-mcp", "authorization_token": "YOUR_TOKEN", "tool_configuration": { "enabled": true, "allowed_tools": ["tool1", "tool2"] } } ] } ``` **After (current):** ```json { "model": "claude-opus-5", "max_tokens": 1000, "messages": [ // ... ], "mcp_servers": [ { "type": "url", "url": "https://mcp.example.com/sse", "name": "example-mcp", "authorization_token": "YOUR_TOKEN" } ], "tools": [ { "type": "mcp_toolset", "mcp_server_name": "example-mcp", "default_config": { "enabled": false }, "configs": { "tool1": { "enabled": true }, "tool2": { "enabled": true } } } ] } ``` ### Common migration patterns | Old pattern | New pattern | | ------------------------------------------- | --------------------------------------------------------------------------------------- | | No `tool_configuration` (all tools enabled) | MCPToolset with no `default_config` or `configs` | | `tool_configuration.enabled: false` | MCPToolset with `default_config.enabled: false` | | `tool_configuration.allowed_tools: [...]` | MCPToolset with `default_config.enabled: false` and specific tools enabled in `configs` | ## Deprecated version: mcp-client-2025-04-04 This version is deprecated. Migrate to `mcp-client-2025-11-20` using the preceding [migration guide](https://platform.claude.com/docs/en/agents-and-tools/mcp-connector#migration-guide). The previous version of the MCP connector included tool configuration directly in the MCP server definition: ```json { "mcp_servers": [ { "type": "url", "url": "https://example-server.modelcontextprotocol.io/sse", "name": "example-mcp", "authorization_token": "YOUR_TOKEN", "tool_configuration": { "enabled": true, "allowed_tools": ["example_tool_1", "example_tool_2"] } } ] } ``` ### Deprecated field descriptions | Property | Type | Description | | ---------------------------------- | ------- | ------------------------------------------------------------------ | | `tool_configuration` | object | **Deprecated:** Use MCPToolset in the `tools` array instead | | `tool_configuration.enabled` | boolean | **Deprecated:** Use `default_config.enabled` in MCPToolset | | `tool_configuration.allowed_tools` | array | **Deprecated:** Use allowlist pattern with `configs` in MCPToolset | --- title: Remote MCP servers url: https://platform.claude.com/docs/en/agents-and-tools/remote-mcp-servers description: Connect Claude to third-party remote MCP servers through the MCP connector API. Browse example servers and review the steps to connect. --- Several companies have deployed remote MCP servers that developers can connect to by using the Anthropic MCP connector API. These servers expand the capabilities available to developers and end users by providing remote access to various services and tools through the MCP protocol. The remote MCP servers listed below are third-party services designed to work with the Claude API. These servers are not owned, operated, or endorsed by Anthropic. Users should only connect to remote MCP servers they trust and should review each server's security practices and terms before connecting. ## Connecting to remote MCP servers To connect to a remote MCP server: 1. Review the documentation for the specific server you want to use. 2. Ensure you have the necessary authentication credentials. 3. Follow the server-specific connection instructions provided by each company. For more information about using remote MCP servers with the Claude API, see [MCP connector](https://platform.claude.com/docs/en/agents-and-tools/mcp-connector). Once connected, remote MCP tools follow the same triggering behavior as any other tool. See [When Claude uses MCP tools](https://platform.claude.com/docs/en/agents-and-tools/mcp-connector#when-claude-uses-mcp-tools). ## Remote MCP server examples **Looking for more?** [Find hundreds more MCP servers on GitHub](https://github.com/modelcontextprotocol/servers). ### MCP > MCP tunnels --- title: MCP tunnels url: https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/overview description: Securely connect Claude to MCP servers running in your private network without opening inbound ports or exposing services to the public internet. --- MCP tunnels let you connect Claude to Model Context Protocol (MCP) servers that run inside your private network. Traffic flows over an outbound-only connection, so you don't need to open inbound firewall ports, expose services to the public internet, or allowlist Anthropic's IP ranges on your origin. MCP tunnels are in research preview. [Request access](https://claude.com/form/claude-managed-agents) to try them. They are provided "as-is" without any uptime, support, or continuity commitment, and they depend on a third-party network provider (Cloudflare) that makes no availability commitment for the underlying transport. Anthropic may modify or discontinue MCP tunnels at any time. For Zero Data Retention and HIPAA BAA eligibility, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#feature-eligibility). ## How it works The [tunnel stack](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#components) is two components that run inside your network: * **[cloudflared](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#components):** Cloudflare's open-source tunnel connector. It initiates outbound-only connections to the [tunnel edge](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#components) and carries encrypted traffic from Anthropic to your proxy. * **[Proxy](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#components):** Anthropic's routing component. It terminates [inner TLS](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#components), validates that upstream IPs fall within an allowed range, and routes each request to the correct [upstream MCP server](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#components) based on hostname. Each MCP server you expose gets a hostname under your tunnel domain (for example, `docs.`). You attach these hostnames to a Managed Agent session in the Claude Console, or pass them to the Messages API through the [MCP connector](https://platform.claude.com/docs/en/agents-and-tools/mcp-connector). ## Prerequisites Before deploying, make sure you have: * A deployment target: a Kubernetes cluster, or a VM with Docker and Docker Compose. * A tunnel. Create one in the Claude Console (see [Create a tunnel](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/console#create-a-tunnel)) or through the API; the Helm chart's setup hook can also create one for you during install. * A way for your stack to authenticate to the Tunnels API. Choose one: * **[Programmatic access](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#credential-provisioning) (recommended).** Set up [Workload Identity Federation](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation) when you create the tunnel. Your stack mints short-lived API tokens from your identity provider, fetches the tunnel token, and generates and registers a CA certificate automatically. Requires permission to manage federation rules, a registered OIDC issuer, and a federation rule with the `workspace:manage_tunnels` scope. * **[Manual](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#credential-provisioning).** Supply static credentials yourself: the tunnel token from the Console and a server certificate signed by a CA you register there. See [Get the connection details](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/console#get-the-connection-details) and [Add a CA certificate](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/console#add-a-ca-certificate). * One or more MCP servers running in your private network. See [Remote MCP servers](https://platform.claude.com/docs/en/agents-and-tools/remote-mcp-servers) for examples. * Outbound connectivity as listed under [Network requirements](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/overview#network-requirements). ### Network requirements | Component | Destination | Port / protocol | Used during | | --------------- | ---------------------------------------------------- | ---------------- | ------------------------------- | | Setup component | `api.anthropic.com` | 443 TCP | Provisioning and token rotation | | cloudflared | Tunnel edge (`198.41.192.0/19`, `2606:4700:a0::/44`) | 7844 TCP and UDP | Runtime | | Proxy | Your upstream MCP servers | As configured | Runtime | ## Security model ### Security layers Three independent layers protect every request: | Layer | Protects against | | --------------------------------------------------------------------------- | ------------------------------------------------------------------------ | | Outer mTLS between Anthropic and the transport provider, with IP validation | Unauthorized clients reaching the tunnel | | Inner TLS from Anthropic's back end to your proxy | Payload inspection by the transport provider or any network intermediary | | OAuth on each MCP server | Unauthorized use of MCP tools by authenticated tunnel traffic | The tunnel transport runs on Cloudflare's network. Because the proxy terminates inner TLS using a certificate that only you hold, Cloudflare cannot read request or response payloads. Anthropic does not connect to a tunnel until a CA certificate is registered, so payloads are always encrypted when they cross Cloudflare's network. Cloudflare does receive connection metadata; see [What the transport provider can observe](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/overview#what-the-transport-provider-can-observe). ### Shared responsibility model | Anthropic handles | Your organization handles | | ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | Tunnel access control | All content and traffic that transits your tunnel, and compliance with applicable third-party acceptable-use policies (including Cloudflare's) | | Validating your CA certificate before connecting to your proxy | Adherence to the deployment guidance on these pages | | Ensuring Claude only sends requests to tunnels owned by your organization | Securing tunnel tokens and TLS private keys | | | Managing the server certificate and renewing it before it expires | | | Configuring OAuth on each MCP server | | | Restricting network access for the proxy and MCP servers | | | Notifying Anthropic if you suspect a breach | If an attacker obtains your tunnel token **and** one of your TLS private keys, they could impersonate your proxy and read MCP request payloads. Treat both as high-value secrets. See [MCP tunnels security](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/security) for hardening guidance. ### What the transport provider can observe Cloudflare provides the outbound transport. It cannot read MCP request or response payloads, but it does receive the following connection metadata: * the egress IP address of the host running cloudflared * a cloudflared host fingerprint * connection timing and byte-volume * the `*.tunnel.anthropic.com` subdomain assigned to your tunnel Anthropic's agreement with Cloudflare restricts Cloudflare's use of this telemetry. Cloudflare acts as a subprocessor for this research preview. ## Deploy a tunnel If you're new to MCP tunnels, start with the quickstart to get a working tunnel locally before configuring a production deployment. The shortest path to a working tunnel: Docker Compose with a sample MCP server. Install on a Kubernetes cluster using the Anthropic Helm chart. Install on a VM using Docker Compose. Choosing between them: * **Deployment target** * **Helm** when deploying to Kubernetes. * **Docker Compose** for a single host or local testing. * **Authentication for setup** * **Programmatic access** (through Workload Identity Federation) when you have an OIDC identity provider such as a Kubernetes cluster, cloud IAM, or SPIFFE. * **Manual credentials** when you don't, or when you're testing. ## Use the tunneled MCP servers Once your tunnel is active (it has an active CA certificate and your tunnel stack is connected), the upstream MCP servers are reachable from Claude Managed Agents and the Messages API. MCP tunnels created through the Console are not available as connectors in claude.ai. In both cases, the tunnel carries encrypted traffic to your MCP server but does not authenticate to it. If the upstream MCP server requires its own authentication (OAuth, bearer token), supply it the same way you would for any other MCP server; it is independent of the tunnel. ### Managed Agents (Console) 1. In **Managed Agents > Sessions**, create a session and choose **Create new agent** so you can edit the MCP server list. 2. Click **+ MCP Server** and open the dropdown. Tunnels in the session's workspace that have at least one active certificate appear at the top of the list, above the public connector catalog. 3. Select the tunnel and supply the **Subdomain** that your proxy routes to a specific MCP server, and the **Path** the upstream MCP server expects. The **Resolves to** line shows the exact URL. ### Messages API Pass the upstream MCP server's URL in the `mcp_servers` array, the same way as any other remote MCP server. The request body and `anthropic-beta` header follow the standard [MCP connector](https://platform.claude.com/docs/en/agents-and-tools/mcp-connector) format; only the `url` is tunnel-specific. The following example uses the MCP connector's `mcp-client` beta header, which is separate from the `mcp-tunnels` beta used by the [Tunnels API](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/reference). Use an API key for the workspace the tunnel was created in (Console **Settings > API keys**). The URL's host is `.`. The path depends on your upstream MCP server, not the tunnel: FastMCP's `streamable-http` transport serves at `/mcp`, and other servers may use `/` or a custom path (check the server's documentation). The proxy forwards the path untouched. ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "Content-Type: application/json" \ -H "X-API-Key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: mcp-client-2025-11-20" \ -d '{ "model": "claude-opus-5", "max_tokens": 1000, "messages": [{"role": "user", "content": "Use the hello tool to greet tunnel."}], "mcp_servers": [ { "type": "url", "url": "https://echo.YOUR_TUNNEL_DOMAIN_HERE/mcp", "name": "echo" } ], "tools": [{"type": "mcp_toolset", "mcp_server_name": "echo"}] }' ``` ```bash CLI ant beta:messages create --beta mcp-client-2025-11-20 <<'YAML' model: claude-opus-5 max_tokens: 1000 messages: - role: user content: Use the hello tool to greet tunnel. mcp_servers: - type: url url: https://echo.YOUR_TUNNEL_DOMAIN_HERE/mcp name: echo tools: - type: mcp_toolset mcp_server_name: echo YAML ``` ```python Python client = anthropic.Anthropic() response = client.beta.messages.create( model="claude-opus-5", max_tokens=1000, messages=[{"role": "user", "content": "Use the hello tool to greet tunnel."}], mcp_servers=[ { "type": "url", "url": "https://echo.YOUR_TUNNEL_DOMAIN_HERE/mcp", "name": "echo", } ], tools=[{"type": "mcp_toolset", "mcp_server_name": "echo"}], betas=["mcp-client-2025-11-20"], ) print(response) ``` ```typescript TypeScript const anthropic = new Anthropic(); const response = await anthropic.beta.messages.create({ model: "claude-opus-5", max_tokens: 1000, messages: [ { role: "user", content: "Use the hello tool to greet tunnel." } ], mcp_servers: [ { type: "url", url: "https://echo.YOUR_TUNNEL_DOMAIN_HERE/mcp", name: "echo" } ], tools: [ { type: "mcp_toolset", mcp_server_name: "echo" } ], betas: ["mcp-client-2025-11-20"] }); console.log(response); ``` ```csharp C# AnthropicClient client = new(); var parameters = new MessageCreateParams { Model = Messages::Model.ClaudeOpus5, MaxTokens = 1000, Messages = new List { new() { Role = Role.User, Content = "Use the hello tool to greet tunnel." } }, McpServers = new List { new() { Url = "https://echo.YOUR_TUNNEL_DOMAIN_HERE/mcp", Name = "echo" } }, Tools = new List { new BetaMcpToolset("echo") }, Betas = ["mcp-client-2025-11-20"] }; var message = await client.Beta.Messages.Create(parameters); Console.WriteLine(message); ``` ```go Go client := anthropic.NewClient() response, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1000, Messages: []anthropic.BetaMessageParam{ anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Use the hello tool to greet tunnel.")), }, MCPServers: []anthropic.BetaRequestMCPServerURLDefinitionParam{ { URL: "https://echo.YOUR_TUNNEL_DOMAIN_HERE/mcp", Name: "echo", }, }, Tools: []anthropic.BetaToolUnionParam{ {OfMCPToolset: &anthropic.BetaMCPToolsetParam{ MCPServerName: "echo", }}, }, Betas: []anthropic.AnthropicBeta{ anthropic.AnthropicBetaMCPClient2025_11_20, }, }) if err != nil { log.Fatal(err) } fmt.Println(response) ``` ```java Java import com.anthropic.models.beta.messages.BetaMcpToolset; // ... import com.anthropic.models.beta.messages.BetaRequestMcpServerUrlDefinition; // ... void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1000L) .addUserMessage("Use the hello tool to greet tunnel.") .addMcpServer(BetaRequestMcpServerUrlDefinition.builder() .url("https://echo.YOUR_TUNNEL_DOMAIN_HERE/mcp") .name("echo") .build()) .addTool(BetaMcpToolset.builder() .mcpServerName("echo") .build()) .addBeta("mcp-client-2025-11-20") .build(); BetaMessage response = client.beta().messages().create(params); IO.println(response); } ``` ```php PHP $client = new Client(); $message = $client->beta->messages->create( maxTokens: 1000, messages: [ ['role' => 'user', 'content' => 'Use the hello tool to greet tunnel.'] ], model: 'claude-opus-5', mcpServers: [ [ 'type' => 'url', 'url' => 'https://echo.YOUR_TUNNEL_DOMAIN_HERE/mcp', 'name' => 'echo', ], ], tools: [ [ 'type' => 'mcp_toolset', 'mcpServerName' => 'echo', ], ], betas: ['mcp-client-2025-11-20'], ); echo $message; ``` ```ruby Ruby client = Anthropic::Client.new response = client.beta.messages.create( model: "claude-opus-5", max_tokens: 1000, messages: [ { role: "user", content: "Use the hello tool to greet tunnel." } ], mcp_servers: [ { type: "url", url: "https://echo.YOUR_TUNNEL_DOMAIN_HERE/mcp", name: "echo" } ], tools: [ { type: "mcp_toolset", mcp_server_name: "echo" } ], betas: ["mcp-client-2025-11-20"] ) puts response ``` For authenticating to the upstream MCP server (`authorization_token`) and other `mcp_servers` options, see [MCP connector](https://platform.claude.com/docs/en/agents-and-tools/mcp-connector). ## Next steps Hardening guidance, credential rotation, and breach response. Diagnose connectivity, TLS, and routing issues. Proxy config fields, the Tunnels API, certificate requirements, and the setup component. Use tunneled servers from the Messages API. --- title: MCP tunnels quickstart url: https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/quickstart description: Connect Claude to a private MCP server using a local Docker Compose deployment. --- MCP tunnels are in research preview. [Request access](https://claude.com/form/claude-managed-agents) to try them. This quickstart takes you from zero to Claude calling a private MCP server through a tunnel. It uses Docker Compose with [manual](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#credential-provisioning) credential provisioning, which is the shortest path for local testing. For production deployments, see [Deploy with Helm](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/deploy-helm) or [Deploy with Docker Compose](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/deploy-compose). ## What you'll build A two-container [tunnel stack](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#components) (the [proxy](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#components) and [cloudflared](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#components)) plus a sample MCP server running alongside it. When everything is running, the sample server is reachable from Claude at `https://echo./mcp` even though nothing is listening on a public port. ## What you need * [Docker and Docker Compose](https://docs.docker.com/get-docker/) on a machine with outbound internet access. * A role in the [Claude Console](https://platform.claude.com) that can manage MCP tunnels. See the [Console guide prerequisites](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/console#prerequisites). * [OpenSSL](https://openssl-library.org/source/) 1.1.1 or later. Preinstalled on macOS and most Linux distributions; on Windows, install it separately (the `openssl` binary must be on your `PATH`). In the Claude Console sidebar, go to **Manage > MCP tunnels** and click **New tunnel**. Give it a name. Leave **Set up programmatic access** off; this quickstart uses manual credential provisioning. After it's created, open the tunnel. Copy two values from the **Connection** section: * **Domain** (looks like `abcd1234.tunnel.anthropic.com`) * **Token** (click the eye icon, then copy) ```bash mkdir -p mcp-tunnel/{config,data} cd mcp-tunnel export TUNNEL_DOMAIN=YOUR_TUNNEL_DOMAIN_HERE # from step 1 export TUNNEL_TOKEN='eyJ...' # from step 1 ``` ```powershell New-Item -ItemType Directory -Force -Path mcp-tunnel/config, mcp-tunnel/data | Out-Null Set-Location mcp-tunnel $env:TUNNEL_DOMAIN = "YOUR_TUNNEL_DOMAIN_HERE" # from step 1 $env:TUNNEL_TOKEN = "eyJ..." # from step 1 ``` The proxy terminates [inner TLS](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#components) using a certificate signed by a CA you control. Generate both: ```bash openssl req -x509 -newkey rsa:2048 -nodes \ -keyout data/ca.key -out data/ca.crt \ -days 3650 -subj "/CN=mcp-tunnel-ca" \ -addext "basicConstraints=critical,CA:TRUE" \ -addext "keyUsage=critical,keyCertSign,cRLSign" \ -addext "subjectKeyIdentifier=hash" cat > data/tls.ext < ```powershell openssl req -x509 -newkey rsa:2048 -nodes ` -keyout data/ca.key -out data/ca.crt ` -days 3650 -subj "/CN=mcp-tunnel-ca" ` -addext "basicConstraints=critical,CA:TRUE" ` -addext "keyUsage=critical,keyCertSign,cRLSign" ` -addext "subjectKeyIdentifier=hash" @" subjectAltName = DNS:$env:TUNNEL_DOMAIN,DNS:*.$env:TUNNEL_DOMAIN authorityKeyIdentifier = keyid,issuer extendedKeyUsage = serverAuth "@ | Set-Content -NoNewline -Encoding ascii -Path data/tls.ext openssl req -newkey rsa:2048 -nodes ` -keyout data/tls.key -out data/server.csr ` -subj "/CN=$env:TUNNEL_DOMAIN" openssl x509 -req -in data/server.csr ` -CA data/ca.crt -CAkey data/ca.key -CAcreateserial ` -out data/tls.crt -days 90 -extfile data/tls.ext ``` Back in the Console, on the tunnel detail page, click **Add certificate** and upload `data/ca.crt` (or paste its contents). The tunnel status flips to **Active**. ```bash cat > hello_server.py <<'EOF' from mcp.server.fastmcp import FastMCP mcp = FastMCP("hello-server", host="0.0.0.0", port=9000) @mcp.tool() def hello(name: str = "world") -> str: """Say hello to someone.""" return f"Hello, {name}!" if __name__ == "__main__": mcp.run(transport="streamable-http") EOF ``` ```powershell @' from mcp.server.fastmcp import FastMCP mcp = FastMCP("hello-server", host="0.0.0.0", port=9000) @mcp.tool() def hello(name: str = "world") -> str: """Say hello to someone.""" return f"Hello, {name}!" if __name__ == "__main__": mcp.run(transport="streamable-http") '@ | Set-Content -NoNewline -Encoding ascii -Path hello_server.py ``` ```bash cat > config/mcp-proxy.yaml < docker-compose.yaml <<'EOF' services: mcp-proxy: image: us-docker.pkg.dev/anthropic-public-registry/images/mcp-proxy@sha256:efb27b299d627e4134815663cb8896641eeaee025d734c0f695582b4df38f013 volumes: - ./config/mcp-proxy.yaml:/etc/mcp-gateway/config.yaml:ro - ./data:/data:ro restart: unless-stopped cloudflared: image: cloudflare/cloudflared@sha256:6b599ca3e974349ead3286d178da61d291961182ec3fe9c505e1dd02c8ac31b0 command: tunnel --no-autoupdate run --url http://localhost:8080 environment: - TUNNEL_TOKEN network_mode: "service:mcp-proxy" restart: unless-stopped hello-mcp: image: python:3.13-slim working_dir: /app volumes: - ./hello_server.py:/app/hello_server.py:ro command: sh -c "pip install --quiet mcp && python hello_server.py" restart: unless-stopped EOF ``` ```powershell @" listen_addr: ":8080" tunnel_domain: $env:TUNNEL_DOMAIN tls: cert_file: /data/tls.crt key_file: /data/tls.key routes: echo: http://hello-mcp:9000 "@ | Set-Content -NoNewline -Encoding ascii -Path config/mcp-proxy.yaml @' services: mcp-proxy: image: us-docker.pkg.dev/anthropic-public-registry/images/mcp-proxy@sha256:efb27b299d627e4134815663cb8896641eeaee025d734c0f695582b4df38f013 volumes: - ./config/mcp-proxy.yaml:/etc/mcp-gateway/config.yaml:ro - ./data:/data:ro restart: unless-stopped cloudflared: image: cloudflare/cloudflared@sha256:6b599ca3e974349ead3286d178da61d291961182ec3fe9c505e1dd02c8ac31b0 command: tunnel --no-autoupdate run --url http://localhost:8080 environment: - TUNNEL_TOKEN network_mode: "service:mcp-proxy" restart: unless-stopped hello-mcp: image: python:3.13-slim working_dir: /app volumes: - ./hello_server.py:/app/hello_server.py:ro command: sh -c "pip install --quiet mcp && python hello_server.py" restart: unless-stopped '@ | Set-Content -NoNewline -Encoding ascii -Path docker-compose.yaml ``` ```bash docker compose up -d docker compose logs mcp-proxy | grep "route configured" docker compose logs cloudflared | grep "Registered tunnel connection" ``` ```powershell docker compose up -d docker compose logs mcp-proxy | Select-String "route configured" docker compose logs cloudflared | Select-String "Registered tunnel connection" ``` You should see one `route configured` line for `echo` and four `Registered tunnel connection` lines. The containers take a few seconds to start; rerun the log commands if they come back empty. In the Console, go to **Managed Agents > Sessions** and create a session. In the agent picker choose **Create new agent**, give the agent a name, and keep the pre-filled model. Click **+ MCP Server**, select your tunnel, set **Subdomain** to `echo` and **Path** to `mcp`. Then ask: > Use the hello tool to greet tunnel. You should see a tool call followed by its result. ## Next steps The tunnel is verified end to end. To swap in your own MCP server, add it to `docker-compose.yaml` (or run it on the same Docker network), add a route for it in `config/mcp-proxy.yaml`, then restart the proxy (`docker compose restart mcp-proxy`). For production deployments: Hardened single-host deployment, with or without programmatic access. Kubernetes deployment with automatic credential management. --- title: Architecture and components url: https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts description: Canonical names for the parts of an MCP tunnel deployment, the two credential-provisioning modes, and the connection model. --- MCP tunnels are in research preview. [Request access](https://claude.com/form/claude-managed-agents) to try them. This page defines the terms used throughout the [MCP tunnels](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/overview) documentation. Several components appear under different names in configuration files, container images, and prose; the following tables give one canonical name for each and list the aliases you may encounter. ## Components | Term | Definition | Also appears as | | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Tunnel stack** | The two containers you run inside your network to attach to a tunnel: the proxy and cloudflared. One stack serves one tunnel and can be replicated across hosts for availability. With programmatic access, the setup component runs alongside the stack to provision credentials. | the stack, the MCP tunnel stack, the tunnel deployment, your deployment | | **Proxy** | Anthropic's routing component. Terminates inner TLS, validates that upstream IPs fall within an allowed range, and routes each request to an upstream MCP server based on hostname. | `mcp-proxy` (image name, Compose service name, and Helm container name), `mcp-gateway` (container-internal config path `/etc/mcp-gateway/config.yaml` and Helm `gateway.config.*` values prefix) | | **cloudflared** | Cloudflare's open-source tunnel connector. Initiates the outbound-only connections from your network to the tunnel edge and carries encrypted traffic between the edge and the proxy. Not related to a Managed Agent. | the outbound connector, the tunnel connector | | **Setup component** | The `setup` binary, shipped inside the `mcp-proxy` image. With programmatic access it authenticates over Workload Identity Federation, fetches the tunnel token, generates a CA and server certificate, and registers the CA with Anthropic. Also provides `renew-cert`. | setup Job (the Helm pre-install hook), `setup` service (the Compose profile), setup hook, setup binary, setup CLI | | **Tunnel edge** | The Cloudflare edge servers that cloudflared dials out to (IP ranges `198.41.192.0/19` and `2606:4700:a0::/44`, port 7844 TCP and UDP). The tunnel that runs over them is provisioned and controlled by Anthropic; Cloudflare operates the underlying network. | the edge, the Anthropic-operated tunnel edge | | **Inner TLS** | A second TLS handshake carried inside the tunnel's plaintext WebSocket stream, between Anthropic's backend and your proxy. The proxy presents a server certificate signed by a CA you registered on the tunnel. Because only you hold the private key, the transport provider cannot read request or response payloads. | the inner TLS handshake | | **Upstream MCP server** | An MCP server running in your private network that the proxy routes to. Each upstream is exposed as one subdomain under your tunnel domain. | upstream, routed MCP server, tunneled MCP server | ## Credential provisioning The tunnel stack needs two credentials at runtime: the **tunnel token**, which authenticates cloudflared's outbound connection, and a **server certificate** signed by a CA registered on the tunnel, which the proxy presents during the inner TLS handshake. There are two ways to supply them, presented throughout this guide as a pair of tabs. | Mode | How credentials reach the stack | Helm chart name | Tab label | | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | ------------------------------- | | **Programmatic access** | The setup component authenticates to the Tunnels API through [Workload Identity Federation](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation), fetches the tunnel token, generates a CA and server certificate locally, and registers the CA. No long-lived secret is copied by hand. Requires a federation rule with the `workspace:manage_tunnels` scope. | Managed mode (`setup.enabled: true`, the default) | **With programmatic access** | | **Manual** | You copy the tunnel token from the Claude Console, generate a CA and server certificate yourself (for example with `openssl`), register the CA in the Console, and supply the token and certificate to the stack as secrets. No setup component runs. | External mode (`setup.enabled: false`) | **Without programmatic access** | These modes are also referred to as **the programmatic flow** and **the manual flow** in the deploy guides. ## Connection model Two directions are at work in a tunnel, and they point opposite ways: * **Connection direction:** cloudflared dials **outbound** from your network to the tunnel edge. Your firewall sees only egress on port 7844; no inbound port is opened. * **Request direction:** once that connection is established, MCP requests travel **from Anthropic toward your network** over it, through cloudflared to the proxy, and on to the upstream MCP server. The phrase "outbound-only" describes the connection, not the requests carried over it. Inner TLS spans Anthropic's backend and your proxy. cloudflared and the tunnel edge sit between them on the wire but see only ciphertext; the proxy is the first place inside your network where MCP request payloads are readable. ```mermaid sequenceDiagram participant A as Anthropic
backend participant E as Tunnel edge
(Cloudflare network) participant C as cloudflared participant P as Proxy participant M as Upstream
MCP server note over C,M: Inside your network C->>E: 1. Outbound connection (port 7844) note over C,E: Connection stays open.
No inbound port is opened. A->>E: 2. MCP request (outer mTLS) E->>C: carried over the open connection C->>P: localhost:8080 note over A,P: Inner TLS spans Anthropic backend to proxy.
Terminates at the proxy. P->>M: 3. Route by hostname M-->>P: response P-->>A: response (same path, reversed) ``` ## See also * [MCP tunnels](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/overview) for the security model and shared-responsibility table. * [MCP tunnels reference](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/reference) for proxy configuration fields, certificate requirements, and the setup component. --- title: Deploy MCP tunnels with Docker Compose url: https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/deploy-compose description: Install the MCP tunnel stack on a VM using Docker Compose. --- MCP tunnels are in research preview. [Request access](https://claude.com/form/claude-managed-agents) to try them. This guide deploys the [tunnel stack](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#components) as hardened containers on a single host. The same configuration can be replicated across multiple hosts for availability. ## Before you begin You need: * **A tunnel.** With programmatic access, the [setup component](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#components) creates one for you when you don't supply a tunnel ID; to attach to an existing tunnel instead, [create it in the Console](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/console#create-a-tunnel) and record the tunnel ID (`tnl_...`). Manual provisioning always starts from a Console-created tunnel. * **A way for the host to authenticate to the Tunnels API.** * **Programmatic access (recommended).** Turn on **Set up programmatic access** when creating the tunnel (or create the federation rule directly under **Settings > Workload identity** if you're letting the setup component create the tunnel) so the setup component can authenticate through Workload Identity Federation. Record the federation rule ID (`fdrl_...`) and your organization ID. * **Manual.** Skip programmatic access. You'll [get the tunnel token from the Console](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/console#get-the-connection-details), generate a CA and server certificate yourself, and [register the CA in the Console](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/console#add-a-ca-certificate). * **A host with Docker and Docker Compose** installed. The manual flow also requires `openssl` (1.1.1 or later). * **Outbound network connectivity** from the host to `api.anthropic.com` (443 TCP) and the [tunnel edge](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#components) (7844 TCP and UDP). See the full [network requirements](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/overview#network-requirements). * **One or more MCP servers** running and reachable from the host on the addresses you'll configure under `routes`. If you don't have one yet, [use the sample server](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/deploy-compose#optional-use-a-sample-mcp-server). ## Optional: Use a sample MCP server If you don't have an MCP server available for testing, use this minimal one: ```bash mkdir -p mcp-tunnel cat > mcp-tunnel/hello_server.py <<'EOF' from mcp.server.fastmcp import FastMCP mcp = FastMCP("hello-server", host="0.0.0.0", port=9000) @mcp.tool() def hello(name: str = "world") -> str: """Say hello to someone.""" return f"Hello, {name}!" if __name__ == "__main__": mcp.run(transport="streamable-http") EOF ``` The following Install steps `cd` into `mcp-tunnel/` and note where to add the corresponding service and route. ## Install This guide provides one reference approach using Docker Compose. You are responsible for adapting it to meet your organization's security requirements. This path requires the host to have an OIDC identity provider (such as a cloud VM metadata server or SPIFFE). If it doesn't, use the **Without programmatic access** tab instead. The setup component uses Workload Identity Federation to fetch the tunnel token, generate a CA and server certificate, and register the CA with Anthropic. ```bash mkdir -p mcp-tunnel/{config,data} cd mcp-tunnel sudo chown 65532:65532 data ``` The containers run as the non-root UID `65532` and need write access to `data/`. The compose file pins images by SHA-256 digest, runs every container as non-root with a read-only filesystem, drops all Linux capabilities, and disables privilege escalation. ```bash cat > docker-compose.yaml <<'EOF' services: setup: image: us-docker.pkg.dev/anthropic-public-registry/images/mcp-proxy@sha256:efb27b299d627e4134815663cb8896641eeaee025d734c0f695582b4df38f013 entrypoint: ["/setup"] command: - init - --api-url=https://api.anthropic.com - --output=dir:/data - --token-version=1 environment: - TUNNEL_ID - ANTHROPIC_FEDERATION_RULE_ID - ANTHROPIC_ORGANIZATION_ID - ANTHROPIC_WORKSPACE_ID - ANTHROPIC_IDENTITY_TOKEN volumes: - ./data:/data user: "65532:65532" read_only: true security_opt: - no-new-privileges:true cap_drop: - ALL profiles: ["setup"] cloudflared: image: cloudflare/cloudflared@sha256:6b599ca3e974349ead3286d178da61d291961182ec3fe9c505e1dd02c8ac31b0 command: tunnel --no-autoupdate run --url http://localhost:8080 environment: - TUNNEL_TOKEN # Share the proxy's netns so localhost:8080 reaches it. network_mode: "service:mcp-proxy" restart: unless-stopped user: "65532:65532" read_only: true security_opt: - no-new-privileges:true cap_drop: - ALL stop_grace_period: 30s logging: options: max-size: "10m" max-file: "3" mcp-proxy: image: us-docker.pkg.dev/anthropic-public-registry/images/mcp-proxy@sha256:efb27b299d627e4134815663cb8896641eeaee025d734c0f695582b4df38f013 volumes: - ./config/mcp-proxy.yaml:/etc/mcp-gateway/config.yaml:ro - ./data:/data:ro restart: unless-stopped user: "65532:65532" read_only: true security_opt: - no-new-privileges:true cap_drop: - ALL stop_grace_period: 30s logging: options: max-size: "10m" max-file: "3" EOF ``` If you're using the [sample MCP server](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/deploy-compose#optional-use-a-sample-mcp-server), append it as a service: ```bash cat >> docker-compose.yaml <<'EOF' hello-mcp: image: python:3.13-slim working_dir: /app volumes: - ./hello_server.py:/app/hello_server.py:ro command: sh -c "pip install --quiet mcp && python hello_server.py" restart: unless-stopped EOF ``` Set the identifiers. Leave `TUNNEL_ID` unset to have the setup component create a tunnel; set it to attach to an existing tunnel from the [Console](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/console#create-a-tunnel): ```bash # export TUNNEL_ID=tnl_... # set to attach to an existing tunnel export ANTHROPIC_FEDERATION_RULE_ID=fdrl_... export ANTHROPIC_ORGANIZATION_ID=00000000-0000-0000-0000-000000000000 ``` If your federation rule is scoped to a workspace other than your organization's default, also set `ANTHROPIC_WORKSPACE_ID=wrkspc_...`; the setup component uses the default workspace otherwise. An auto-created tunnel is created in that workspace. Set `ANTHROPIC_IDENTITY_TOKEN` to an OIDC JWT from this host's identity provider. Follow the [WIF guide for your provider](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation#identity-providers) to register the issuer, set the rule's subject, and mint the token; the rule's audience must match the audience you request when minting. Run the setup component: ```bash docker compose run --rm setup ``` `setup init` is idempotent over `data/`: re-running it reuses the tunnel ID and CA already stored there and never creates a second tunnel. A new CA is generated and registered only when `data/` is empty or `TUNNEL_ID` has changed; in that case the cap of two active certificates applies, so revoke one in the Console first if both slots are filled. See [Setup component authentication failures](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/troubleshooting#setup-component-authentication-failures) if it errors. Retrieve your tunnel domain and export it for later steps: ```bash export TUNNEL_DOMAIN=$(sudo cat data/tunnel-domain) echo "$TUNNEL_DOMAIN" ``` Workload Identity Federation tokens are short-lived (1 hour by default) and expire automatically; there is nothing to revoke after setup completes. `tunnel_domain` is **required**: the [proxy](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#components) uses it to strip the domain suffix from incoming hostnames before looking up the subdomain in `routes`. `routes` is a flat map from subdomain to upstream URL, not a list. ```bash cat > config/mcp-proxy.yaml < ```bash export TUNNEL_TOKEN=$(sudo cat data/tunnel-token) docker compose up -d ``` Use this flow if you didn't turn on **Set up programmatic access**, or for local development and testing. There is no `setup` service. On the tunnel detail page, copy the **Domain** (it has the form `abcd1234.tunnel.anthropic.com`), then click the eye icon next to **Token** to fetch the tunnel token and use the copy icon to copy it. Set both as shell variables for the rest of the guide: ```bash export TUNNEL_DOMAIN=YOUR_TUNNEL_DOMAIN_HERE export TUNNEL_TOKEN='eyJ...' ``` ```bash mkdir -p mcp-tunnel/{data,config} cd mcp-tunnel ``` The proxy listens on `:8080` over plain WebSocket; the [inner TLS](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#components) handshake happens **inside** that WebSocket stream using these certificates. Anthropic verifies the inner handshake against the CA you register in the Console. The server certificate's Subject Alternative Name (SAN) must include `*.` per the [certificate requirements](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/reference#certificate-requirements). ```bash # Self-signed CA. Explicit extensions so it satisfies the certificate # requirements regardless of distro openssl.cnf defaults. openssl req -x509 -newkey rsa:2048 -nodes \ -keyout data/ca.key -out data/ca.crt \ -days 3650 -subj "/CN=mcp-tunnel-ca" \ -addext "basicConstraints=critical,CA:TRUE" \ -addext "keyUsage=critical,keyCertSign,cRLSign" \ -addext "subjectKeyIdentifier=hash" # Extension file for the server certificate. Using -extfile (instead of # -copy_extensions, which is OpenSSL 3.0+ only) keeps this working on # OpenSSL 1.1.x. cat > data/tls.ext < On the tunnel detail page, scroll to the **Certificates** section and click **Add certificate**. Upload `data/ca.crt` directly with **Choose file** (the modal accepts `.pem`, `.crt`, and `.cer`), or paste its contents: ```bash cat data/ca.crt ``` The tunnel's status flips to **Active** once a certificate is registered. See [Add a CA certificate](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/console#add-a-ca-certificate). `tunnel_domain` is **required**: the proxy uses it to strip the domain suffix from incoming hostnames before looking up the subdomain in `routes`. `routes` is a flat map from subdomain to upstream URL, not a list. ```bash cat > config/mcp-proxy.yaml < The `network_mode: "service:mcp-proxy"` setting places [cloudflared](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#components) in the proxy's network namespace so that `localhost:8080` inside the cloudflared container reaches the proxy. The `--url http://localhost:8080` flag gives cloudflared its forwarding target; without that flag, cloudflared has no route for incoming requests and returns a 503. ```bash cat > docker-compose.yaml <<'EOF' services: cloudflared: image: cloudflare/cloudflared@sha256:6b599ca3e974349ead3286d178da61d291961182ec3fe9c505e1dd02c8ac31b0 # --url is required: no ingress rules are pushed in the manual flow, # so without it cloudflared 503s every request. command: tunnel --no-autoupdate run --url http://localhost:8080 environment: - TUNNEL_TOKEN # Share the proxy's netns so localhost:8080 reaches it. network_mode: "service:mcp-proxy" restart: unless-stopped user: "65532:65532" read_only: true security_opt: - no-new-privileges:true cap_drop: - ALL stop_grace_period: 30s logging: options: max-size: "10m" max-file: "3" mcp-proxy: image: us-docker.pkg.dev/anthropic-public-registry/images/mcp-proxy@sha256:efb27b299d627e4134815663cb8896641eeaee025d734c0f695582b4df38f013 volumes: - ./config/mcp-proxy.yaml:/etc/mcp-gateway/config.yaml:ro - ./data:/data:ro restart: unless-stopped user: "65532:65532" read_only: true security_opt: - no-new-privileges:true cap_drop: - ALL stop_grace_period: 30s logging: options: max-size: "10m" max-file: "3" EOF ``` If you're using the [sample MCP server](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/deploy-compose#optional-use-a-sample-mcp-server), append it as a service: ```bash cat >> docker-compose.yaml <<'EOF' hello-mcp: image: python:3.13-slim working_dir: /app volumes: - ./hello_server.py:/app/hello_server.py:ro command: sh -c "pip install --quiet mcp && python hello_server.py" restart: unless-stopped EOF ``` ```bash docker compose up -d ``` The compose file reads `TUNNEL_TOKEN` from the host environment with no default, so the export must be repeated in every fresh shell and after a reboot. For a multi-VM deployment, copy the `mcp-tunnel/` directory to each host, set `TUNNEL_TOKEN`, and run `docker compose up -d`. In the programmatic flow `TUNNEL_TOKEN` is `$(sudo cat data/tunnel-token)`; in the manual flow it's the value you copied from the Console. The same tunnel token and certificates work across all replicas. ## Verify the deployment Verify end to end by calling an [upstream MCP server](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#components) from Anthropic's side: see [Use the tunneled MCP servers](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/overview#use-the-tunneled-mcp-servers). With the [sample MCP server](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/deploy-compose#optional-use-a-sample-mcp-server), the routed URL is `https://echo./mcp`. If verification fails, see [Troubleshooting](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/troubleshooting). ## Upgrades Run the commands in this section from inside the `mcp-tunnel/` deployment directory. ### Rotate the tunnel token With programmatic access, increment `--token-version` in the `setup` service command, set the Workload Identity Federation identifiers, mint a fresh OIDC JWT, and re-run the setup component: ```bash # Edit docker-compose.yaml: increment the integer in the setup service's # --token-version argument (for example, --token-version=1 to # --token-version=2). The setup binary refuses to rotate when the value # hasn't changed. # export TUNNEL_ID=tnl_... # set only if you set it during install export ANTHROPIC_FEDERATION_RULE_ID=fdrl_... export ANTHROPIC_ORGANIZATION_ID=00000000-0000-0000-0000-000000000000 # export ANTHROPIC_WORKSPACE_ID=wrkspc_... # if your rule is workspace-scoped # Re-mint ANTHROPIC_IDENTITY_TOKEN per the WIF provider guide for your # environment (it will have expired since install). export ANTHROPIC_IDENTITY_TOKEN=... docker compose run --rm setup export TUNNEL_TOKEN=$(sudo cat data/tunnel-token) docker compose up -d cloudflared ``` The `--token-version` argument is edited in `docker-compose.yaml` rather than passed on the command line so the new value persists for future runs of the setup component. The setup component authenticates with Workload Identity Federation; there is no API token to revoke. Without programmatic access, click **Rotate token** on the tunnel detail page in the Console, then update the `TUNNEL_TOKEN` environment variable on each host and restart cloudflared (`docker compose up -d cloudflared`). Clicking **Rotate token** invalidates the current token immediately. Between that moment and updating `TUNNEL_TOKEN` on every host and restarting cloudflared, any host whose cloudflared restarts (crash, host reboot) cannot reconnect. Update each host promptly after rotating. ### Certificate renewal You're responsible for monitoring expiry and renewing the server certificate before it expires. With programmatic access: ```bash docker compose run --rm setup renew-cert --output=dir:/data ``` The CLI arguments replace the `setup` service's `command` (the `init` arguments) but keep its `entrypoint`, so this runs `/setup renew-cert --output=dir:/data`. Pass `--renew-before=720h` to make the command a no-op when more than 30 days of validity remain. This makes it safe to run on a fixed schedule. Without programmatic access, sign a new server certificate with your existing CA (the CA registered in the Console doesn't change) and replace `data/tls.crt`. Set `TUNNEL_DOMAIN` first if you're running this from a fresh shell. ```bash export TUNNEL_DOMAIN=YOUR_TUNNEL_DOMAIN_HERE openssl req -new -key data/tls.key -out /tmp/server.csr \ -subj "/CN=${TUNNEL_DOMAIN}" openssl x509 -req -in /tmp/server.csr \ -CA data/ca.crt -CAkey data/ca.key -CAcreateserial \ -out data/tls.crt -days 90 \ -extfile data/tls.ext ``` In either flow the proxy polls `tls.cert_file` and reloads it automatically, so no restart is required. ## Next steps Attach an upstream MCP server to a Managed Agent or the Messages API. Hardening guidance, credential rotation, and breach response. Diagnose connectivity, TLS, and routing issues. --- title: Deploy MCP tunnels with Helm url: https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/deploy-helm description: Install the tunnel stack on a Kubernetes cluster using the Anthropic Helm chart. --- MCP tunnels are in research preview. [Request access](https://claude.com/form/claude-managed-agents) to try them. The Anthropic Helm chart installs the [tunnel stack](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#components) as a single Deployment and attaches it to your tunnel: one the chart's setup hook creates for you, or an existing tunnel you created in the [Console](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/console#create-a-tunnel). ## Before you begin You need: * **A tunnel.** With programmatic access, the chart's setup hook creates one for you when you don't supply a tunnel ID; to attach to an existing tunnel instead, [create it in the Console](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/console#create-a-tunnel) and record the tunnel ID (`tnl_...`). Manual provisioning always starts from a Console-created tunnel; you'll also need its tunnel token and tunnel domain. * **A way for the chart to authenticate to the Tunnels API.** * **[Programmatic access](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#credential-provisioning) (recommended).** The [setup component](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#components) authenticates through Workload Identity Federation, fetches the tunnel token, generates a CA, registers it with Anthropic, and stores everything in a Secret. You'll need a federation rule scoped to `workspace:manage_tunnels`. * **[Manual](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#credential-provisioning).** Skip programmatic access. You'll [get the tunnel token from the Console](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/console#get-the-connection-details), generate a CA and server certificate yourself, [register the CA in the Console](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/console#add-a-ca-certificate), and supply the credentials to the cluster as Secrets. * **A Kubernetes cluster** you can deploy to with `helm` and `kubectl`. The **Without programmatic access** tab also uses `openssl` (1.1.1 or later). * **Outbound network connectivity** from the cluster to `api.anthropic.com` (443 TCP) and the [tunnel edge](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#components) (7844 TCP and UDP). See the full [network requirements](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/overview#network-requirements). * **One or more MCP servers** running and reachable from the cluster on the addresses you'll configure under `gateway.config.routes`. If you don't have one yet, [use the sample server](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/deploy-helm#optional-use-a-sample-mcp-server). ## Optional: Use a sample MCP server If you don't have an MCP server available for testing, use this minimal one: ```bash kubectl create namespace mcp-tunnel --dry-run=client -o yaml | kubectl apply -f - kubectl -n mcp-tunnel apply -f - <<'EOF' apiVersion: v1 kind: ConfigMap metadata: name: hello-mcp-src data: hello_server.py: | from mcp.server.fastmcp import FastMCP mcp = FastMCP("hello-server", host="0.0.0.0", port=9000) @mcp.tool() def hello(name: str = "world") -> str: """Say hello to someone.""" return f"Hello, {name}!" if __name__ == "__main__": mcp.run(transport="streamable-http") --- apiVersion: apps/v1 kind: Deployment metadata: name: hello-mcp spec: replicas: 1 selector: matchLabels: { app: hello-mcp } template: metadata: labels: { app: hello-mcp } spec: containers: - name: hello-mcp image: python:3.13-slim command: ["sh", "-c", "pip install --quiet mcp && python /app/hello_server.py"] volumeMounts: - { name: src, mountPath: /app } ports: - { containerPort: 9000 } volumes: - name: src configMap: { name: hello-mcp-src } --- apiVersion: v1 kind: Service metadata: name: hello-mcp spec: selector: { app: hello-mcp } ports: - { port: 9000, targetPort: 9000 } EOF ``` The Install steps that follow note where to add the corresponding route. ## Install The setup component exchanges the cluster's projected ServiceAccount token through your federation rule, fetches the tunnel token, generates a CA and server certificate, and registers the CA with Anthropic. A daily CronJob renews the server certificate as needed, so you don't handle any secrets by hand. Follow [Use WIF with Kubernetes](https://platform.claude.com/docs/en/manage-claude/wif-providers/kubernetes) to register your cluster's OIDC issuer and create a federation rule. The setup component runs under its own ServiceAccount in the release namespace; the exact name follows Helm's `fullname` convention, so for any release name other than `mcp-tunnel`, run `helm template ... | grep -A2 'kind: ServiceAccount'` to confirm it before creating the rule. The rest of this guide assumes release name `mcp-tunnel` in namespace `mcp-tunnel`, where the ServiceAccount is `mcp-tunnel-setup`. | Field | Value | | -------- | ---------------------------------------------------- | | Subject | `system:serviceaccount:mcp-tunnel:mcp-tunnel-setup` | | Audience | `api.anthropic.com` (the chart's default; no scheme) | | Scope | `workspace:manage_tunnels` | The chart's default audience is `api.anthropic.com` with no scheme, but the Console's federation-rule form suggests `https://api.anthropic.com`. The two must match byte-for-byte or authentication fails. Either set the rule's audience to `api.anthropic.com`, or set `api.wif.audience` in `values.yaml` to `https://api.anthropic.com`. If the tunnel is in a workspace other than the organization's default, also add the rule's service account as a member of that workspace under **Settings > Workspaces** (the Tunnels API authorizes against the service account's workspace memberships). Note the rule's ID (`fdrl_...`); you'll set it as `api.wif.federationRuleId`. The daily certificate-renewal CronJob uses a separate ServiceAccount (also derived from the Helm `fullname`) but does not call the Tunnels API; it renews the certificate locally and only needs Kubernetes RBAC, which the chart grants. The federation rule does not need to cover it. ```bash helm show values \ oci://us-docker.pkg.dev/anthropic-public-registry/charts/mcp-tunnel \ --version 2.0.2 > values.yaml ``` Edit `values.yaml` and set the `api.wif.*` keys with the federation rule ID and organization ID, plus a `routes` entry for each [upstream MCP server](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#components): ```yaml values.yaml api: wif: federationRuleId: "fdrl_..." organizationId: "00000000-0000-0000-0000-000000000000" # Set when the tunnel is in a non-default workspace and the # rule's service account is a member of that workspace. # workspaceId: "wrkspc_..." tunnel: # Leave empty to have the setup hook create a tunnel during install. # Set to attach to an existing tunnel from the Console. id: "" # Increment to rotate the tunnel token on the next upgrade. # See the "Rotate the tunnel token" section. tokenVersion: "1" gateway: config: routes: docs: http://docs-mcp.internal:8080 search: http://search-mcp.internal:8080 ``` With these routes, Claude reaches the servers at `docs.` and `search.`. Some managed Kubernetes distributions allocate the Service CIDR outside the standard private ranges; if your routes target in-cluster Services, add `gateway.config.upstream.allowed_ips` here per [Upstream IP validation](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/troubleshooting#upstream-ip-validation). If you're using the [sample MCP server](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/deploy-helm#optional-use-a-sample-mcp-server), set `routes` to `echo: http://hello-mcp:9000` instead. Render the chart and review the output according to your organization's vetting practices: ```bash helm template mcp-tunnel \ oci://us-docker.pkg.dev/anthropic-public-registry/charts/mcp-tunnel \ --version 2.0.2 \ -n mcp-tunnel \ -f values.yaml > rendered.yaml ``` ```bash helm install mcp-tunnel \ oci://us-docker.pkg.dev/anthropic-public-registry/charts/mcp-tunnel \ --version 2.0.2 \ --namespace mcp-tunnel --create-namespace \ -f values.yaml ``` The setup component runs as a Helm pre-install hook Job, so `helm install` blocks until it completes. On success Helm deletes the Job automatically. If `helm install` fails with a hook error, see [Setup component authentication failures](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/troubleshooting#setup-component-authentication-failures). When `tunnel.id` is empty, the setup component creates the tunnel in the workspace your federation rule targets (the organization's default workspace unless you set `api.wif.workspaceId`) and stores its ID and domain in the `mcp-tunnel` Secret. Find the domain you'll need for [verification](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/deploy-helm#verify-the-deployment) on the tunnel's detail page in the Console under **Manage > MCP tunnels**, or read it from the Secret: ```bash kubectl -n mcp-tunnel get secret mcp-tunnel \ -o jsonpath='{.data.tunnel-domain}' | base64 -d ``` Re-running the setup component (during [upgrades](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/deploy-helm#upgrades) or [token rotation](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/deploy-helm#rotate-the-tunnel-token)) reuses the tunnel ID stored in this Secret; it never creates a second tunnel. The `api.wif.*` values are identifiers, not secrets, so storing them in Helm release-history Secrets is not a risk. The sensitive data at rest is the `mcp-tunnel` Secret the setup component creates, which holds the tunnel token and TLS private keys. Apply your organization's standard practices for protecting Kubernetes Secrets to this namespace. In this mode (`setup.enabled: false`) the chart makes no API calls; the setup component does not run and there is no cert-renew CronJob. Use this path if you'd rather not set up Workload Identity Federation. [Create the tunnel](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/console#create-a-tunnel) and [get the tunnel token from the Console](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/console#get-the-connection-details). Record the tunnel domain from the detail page. You'll set it as `gateway.config.tunnel_domain`. The proxy listens on plain WebSocket, with [inner TLS](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#components) carried inside that stream using the certificate you generate here. The server certificate's SAN must include `*.` per the [certificate requirements](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/reference#certificate-requirements). ```bash export TUNNEL_DOMAIN=YOUR_TUNNEL_DOMAIN_HERE mkdir -p mcp-tunnel/data cd mcp-tunnel # Self-signed CA. Explicit extensions so it satisfies the certificate # requirements regardless of distro openssl.cnf defaults. openssl req -x509 -newkey rsa:2048 -nodes \ -keyout data/ca.key -out data/ca.crt \ -days 3650 -subj "/CN=mcp-tunnel-ca" \ -addext "basicConstraints=critical,CA:TRUE" \ -addext "keyUsage=critical,keyCertSign,cRLSign" \ -addext "subjectKeyIdentifier=hash" # Extension file for the server certificate. Using -extfile (instead of # -copy_extensions, which is OpenSSL 3.0+ only) keeps this working on # OpenSSL 1.1.x. cat > data/tls.ext < The chart reads specific keys; the Secret names are configurable but the keys are not. The following namespace-creation command is a no-op if the namespace already exists (for example, from the [sample MCP server](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/deploy-helm#optional-use-a-sample-mcp-server) step). ```bash kubectl create namespace mcp-tunnel --dry-run=client -o yaml | kubectl apply -f - kubectl -n mcp-tunnel create secret generic mcp-tunnel-token \ --from-literal=tunnel-token='eyJ...' kubectl -n mcp-tunnel create secret generic mcp-tunnel-cert \ --from-file=tls.crt=data/tls.crt \ --from-file=tls.key=data/tls.key ``` ```bash helm show values \ oci://us-docker.pkg.dev/anthropic-public-registry/charts/mcp-tunnel \ --version 2.0.2 > values.yaml ``` Edit `values.yaml` and set the following keys: ```yaml values.yaml setup: enabled: false external: tunnelTokenSecretName: mcp-tunnel-token # must contain key: tunnel-token serverCertSecretName: mcp-tunnel-cert # must contain keys: tls.crt, tls.key gateway: config: # Required when setup.enabled is false. Replace the placeholder with # the $TUNNEL_DOMAIN value you exported earlier. When setup.enabled # is true the chart injects this from the Secret as a -tunnel-domain # flag instead. tunnel_domain: YOUR_TUNNEL_DOMAIN_HERE routes: docs: http://docs-mcp.internal:8080 search: http://search-mcp.internal:8080 ``` Some managed Kubernetes distributions allocate the Service CIDR outside the standard private ranges; if your routes target in-cluster Services, add `gateway.config.upstream.allowed_ips` here per [Upstream IP validation](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/troubleshooting#upstream-ip-validation). If you're using the [sample MCP server](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/deploy-helm#optional-use-a-sample-mcp-server), set `routes` to `echo: http://hello-mcp:9000` instead. ```bash helm template mcp-tunnel \ oci://us-docker.pkg.dev/anthropic-public-registry/charts/mcp-tunnel \ --version 2.0.2 \ -n mcp-tunnel \ -f values.yaml > rendered.yaml ``` ```bash helm install mcp-tunnel \ oci://us-docker.pkg.dev/anthropic-public-registry/charts/mcp-tunnel \ --version 2.0.2 \ --namespace mcp-tunnel --create-namespace \ -f values.yaml ``` ## Verify the deployment Verify end to end from Anthropic's side: use `https://./` in a Managed Agent session or a Messages API request, where `` is a key from `gateway.config.routes` and `` is whatever the upstream MCP server serves at. With the [sample MCP server](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/deploy-helm#optional-use-a-sample-mcp-server), that's `https://echo./mcp`. See [Use the tunneled MCP servers](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/overview#use-the-tunneled-mcp-servers) for the request shapes. If that fails, check the pod logs (`kubectl -n mcp-tunnel logs deploy/mcp-tunnel -c mcp-proxy` and `-c cloudflared`) and consult [Troubleshooting](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/troubleshooting). ## Optional configuration ### Restrict egress with NetworkPolicy Ingress to the proxy pod is denied by default (`networkPolicy.ingress.enabled: true`). To additionally restrict pod egress, set `networkPolicy.egress.enabled: true` and populate `networkPolicy.egress.mcpServers` with pod label selectors or CIDR ranges that cover your upstream MCP servers. Egress from cloudflared to the tunnel edge is allowed separately through `networkPolicy.egress.cloudflaredEgressCIDRs`. ### Tune the proxy Fields under `gateway.config.*` pass through to the proxy configuration file. Common adjustments include `upstream.allowed_ips`, `log_level`, and `upstream.tls`. See the [proxy configuration](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/reference#proxy-configuration) reference for the full field list. The chart always sets `listen_addr`, `tls.cert_file`, and `tls.key_file`; setting them in `gateway.config` has no effect. ### Supply your own OIDC token By default the chart projects a Kubernetes ServiceAccount token for the setup component. To use a token from a different identity provider (such as [SPIFFE](https://platform.claude.com/docs/en/manage-claude/wif-providers/spiffe), Vault, or a cloud-SDK sidecar), mount it with `setup.extraVolumes` and `setup.extraVolumeMounts`. Then point `api.wif.tokenFile` at the mount path. The chart sets `ANTHROPIC_IDENTITY_TOKEN_FILE` to that path, and the setup component reads the token from there. ## Upgrades Always pass `--version` to `helm upgrade` so you don't pull a newer chart unexpectedly. ### Upgrade from chart 1.x Chart 2.0.0 moves the tunnel ID from `api.wif.tunnelId` to `tunnel.id`. Before upgrading, edit your `values.yaml`: move the `tnl_...` value to `tunnel.id` and remove `api.wif.tunnelId`. Leaving `tunnel.id` unset is safe (the setup component reuses the tunnel ID already stored in the `mcp-tunnel` Secret on re-run), but the explicit move keeps your `values.yaml` accurate. Also update your federation rule's scope from `org:manage_tunnels` to `workspace:manage_tunnels` in the Console. ### Change configuration For routine changes such as routes, replica count, or NetworkPolicy: ```bash helm upgrade mcp-tunnel \ oci://us-docker.pkg.dev/anthropic-public-registry/charts/mcp-tunnel \ --version 2.0.2 \ -n mcp-tunnel \ -f values.yaml ``` Maintain a complete `values.yaml` rather than relying on `--reuse-values`. Helm's deep-merge behavior can silently fail to remove deleted routes. ### Rotate the tunnel token With programmatic access, increment `tunnel.tokenVersion` in `values.yaml` and upgrade with `--set setup.force=true`. The setup component only re-runs on upgrades when forced: ```bash helm upgrade mcp-tunnel \ oci://us-docker.pkg.dev/anthropic-public-registry/charts/mcp-tunnel \ --version 2.0.2 \ -n mcp-tunnel \ -f values.yaml \ --set setup.force=true ``` The setup component authenticates with Workload Identity Federation; there is no API token to revoke. Without programmatic access, click **Rotate token** on the tunnel detail page in the Console, then update the `mcp-tunnel-token` Secret: ```bash kubectl -n mcp-tunnel create secret generic mcp-tunnel-token \ --from-literal=tunnel-token='eyJ...' --dry-run=client -o yaml | kubectl apply -f - kubectl -n mcp-tunnel rollout restart deploy/mcp-tunnel ``` Clicking **Rotate token** invalidates the current token immediately. Until the Secret is updated and the rollout completes, any pod that restarts with the old token (eviction, node drain, OOM) cannot reconnect. Update the Secret promptly after rotating; for stricter availability requirements, use programmatic access so the chart handles the rotation atomically. ### Certificate renewal The chart provides automation, but you remain responsible for monitoring expiry and confirming renewal completes. With programmatic access, certificate renewal is automatic. The chart deploys a CronJob (named after the Helm `fullname`, suffixed `-cert-renew`) that runs `setup renew-cert` daily (at `serverCert.cronSchedule`, default `0 0 * * *` UTC). The job is a no-op unless the certificate is within `serverCert.renewBefore` of expiry (default 30 days). Renewal is local: the job signs a fresh certificate with the CA already stored in the Secret, makes no API calls, and only needs the Kubernetes RBAC the chart grants. The proxy hot-reloads the certificate from the Secret mount, so no Deployment restart is needed. Without programmatic access there is no CronJob. From inside the `mcp-tunnel/` directory you kept after install, sign a fresh server certificate with the existing CA (do not regenerate the CA): ```bash export TUNNEL_DOMAIN=YOUR_TUNNEL_DOMAIN_HERE openssl req -new -key data/tls.key -out /tmp/server.csr \ -subj "/CN=${TUNNEL_DOMAIN}" openssl x509 -req -in /tmp/server.csr \ -CA data/ca.crt -CAkey data/ca.key -CAcreateserial \ -out data/tls.crt -days 90 -extfile data/tls.ext kubectl -n mcp-tunnel create secret generic mcp-tunnel-cert \ --from-file=tls.crt=data/tls.crt --from-file=tls.key=data/tls.key \ --dry-run=client -o yaml | kubectl apply -f - ``` The proxy hot-reloads the certificate from the Secret mount. ## Next steps Attach an upstream MCP server to a Managed Agent or the Messages API. Hardening guidance, credential rotation, and breach response. Diagnose connectivity, TLS, and routing issues. --- title: Manage tunnels in the Console url: https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/console description: Create tunnels, register CA certificates, retrieve the tunnel token, and attach tunneled MCP servers to agents from the Claude Console. --- MCP tunnels are in research preview. [Request access](https://claude.com/form/claude-managed-agents) to try them. This page covers the Console side of an MCP tunnels deployment: creating a tunnel, registering your CA certificate, retrieving the tunnel token, and attaching the [upstream MCP servers](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#components) to an agent. [Deploy MCP tunnels with Helm](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/deploy-helm) and [Deploy MCP tunnels with Docker Compose](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/deploy-compose) cover running the [tunnel stack](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#components) inside your network. ## Prerequisites * **One or more MCP servers** running in your private network. The tunnel routes traffic to them; it does not host them. See [Remote MCP servers](https://platform.claude.com/docs/en/agents-and-tools/remote-mcp-servers) for examples you can deploy. * **A Console role with the Manage tunnels permission**, so you can create and archive tunnels, rotate the token, and manage certificates. Organization admins and owners have it by default; custom roles and per-account grants can also include it. Roles without it have read-only access to the **MCP tunnels** page and tunnel details. * **A way for your stack to authenticate to the Tunnels API.** Choose one: * **[Programmatic access](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#credential-provisioning) (recommended).** Set up [Workload Identity Federation](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation) during tunnel creation so your stack mints short-lived API tokens from your identity provider, fetches the tunnel token, and generates and registers a CA certificate automatically. Requires permission to manage federation rules, a registered OIDC issuer, and a federation rule with the `workspace:manage_tunnels` scope. * **[Manual](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#credential-provisioning).** Skip programmatic access. After creating the tunnel, [get the tunnel token](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/console#get-the-connection-details), generate and [register a CA certificate](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/console#add-a-ca-certificate) yourself, and supply the token and your server certificate to your tunnel stack as secrets. ## Create a tunnel In the Console sidebar, go to **Manage > MCP tunnels**. Tunnels are workspace-scoped; the new tunnel belongs to the workspace currently selected in the Console, so switch workspaces first if you want it elsewhere. Click **New tunnel** and enter a name in the **Create tunnel** dialog. The name is required and identifies the tunnel in the list, on the detail page, and in the agent MCP server picker. A domain of the form `abcd1234.tunnel.anthropic.com` is assigned automatically. If your role can manage federation rules, a **Set up programmatic access** toggle appears (off by default). If not, the Console shows a notice in its place and your tunnel stack uses the manual flow instead. The rest of the create flow is the same either way. Programmatic access relies on [Workload Identity Federation](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation); read that page first if federation issuers, rules, and service accounts are unfamiliar. To turn the toggle on you need: 1. **A registered OIDC issuer** for the identity provider your stack presents tokens from (such as a Kubernetes cluster, AWS IAM, Google Cloud, or GitHub Actions). Register one under **Settings > Workload identity > Issuers** if your organization doesn't have one. 2. **A federation rule with the `workspace:manage_tunnels` scope.** Turning on the toggle reveals a **Federation rule** picker. Choose an existing rule with that scope, or click **Create federation rule** to create one inline. 3. **The rule's service account added to this workspace.** The Tunnels API authorizes against the service account's workspace memberships. If you're creating the tunnel in a workspace other than the organization's default, add the service account under **Settings > Workspaces** and pass the workspace ID at deploy time (`api.wif.workspaceId` for Helm, `ANTHROPIC_WORKSPACE_ID` for Compose). Skipping this step is fully supported; both deploy guides have a **Without programmatic access** tab. Click **Create tunnel**. The Console provisions the tunnel and opens the detail page. Both deploy paths need: * The **tunnel ID** (`tnl_...`), shown on the tunnel detail page. * The **tunnel domain** (`abcd1234.tunnel.anthropic.com`), shown on the tunnel detail page. Used as the proxy's `tunnel_domain` and in the server certificate's SAN. What else you need depends on the [credential-provisioning mode](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#credential-provisioning): | With programmatic access | Without programmatic access | | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | The **federation rule ID** (`fdrl_...`) of the rule you selected. The rule is org-level, not stored on the tunnel; find it under **Settings > Workload identity > Rules**. | The **tunnel token**, revealed with the eye icon next to **Token** on the detail page. Treat it as a secret. See [Get the connection details](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/console#get-the-connection-details). | | The **organization ID** (a UUID), shown under **Settings > Organization**. | A **CA certificate** that you generate and [register on the tunnel](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/console#add-a-ca-certificate). | With programmatic access, your stack fetches the tunnel token through the Tunnels API, generates the CA and server certificate locally (the private key never leaves your environment), and registers only the CA's public certificate with Anthropic. You're still responsible for securing the private keys and renewing the server certificate before it expires. Your organization can have up to 10 active tunnels. Creating a tunnel does not establish any connectivity; that happens once your stack dials in with the tunnel token and a CA certificate is registered. ## Get the connection details Open the tunnel. The detail page shows a **Connection** section with the domain and token and a **Certificates** section. | Field | Description | | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Domain** | Copy the assigned `abcd1234.tunnel.anthropic.com` value. Your proxy's routes are subdomains of this domain. | | **Token** | Click the eye icon (**Show token**) to fetch the tunnel token, then use the copy icon to copy it into your tunnel stack's secret store. Click **Rotate token** to invalidate the current token and issue a new one. | Every reveal and rotation is recorded in your organization's [Compliance API](https://platform.claude.com/docs/en/manage-claude/compliance-api) activity log. Rotation does not sever cloudflared connections that are already established, so you can rotate, redeploy with the new value, and let the old connections drain. ## Add a CA certificate Anthropic verifies [inner TLS](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#components) to your [proxy](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#components) against the CA certificates you register on the tunnel. A tunnel with no active certificates cannot accept connections, and does not appear in the agent MCP server picker until one is registered. On the tunnel's detail page, scroll to the **Certificates** section and click **Add certificate**. Click **Choose file** to select a `.pem`, `.crt`, or `.cer` file, drag the file onto the text area, or paste the PEM block directly. The modal rejects private-key material and content that isn't a `-----BEGIN CERTIFICATE-----` block. The file must be 8 kB or smaller. Click **Add certificate**. The fingerprint and expiry appear in the certificate list, and the slot count on the section header increments. A tunnel holds up to two active certificates so you can rotate without downtime: register the new certificate alongside the old one, redeploy your proxy with the new key pair, confirm traffic is flowing, then click **Revoke** on the old certificate's row. Revoked certificates remain visible in the list with a **Revoked** badge. ## Deploy the tunnel stack The tunnel exists in the Console, but no traffic flows until the tunnel stack is running inside your network and dialed in with the tunnel token. Follow one of the deploy guides: Run the tunnel stack on a single host. Both programmatic-access and manual flows. Run the tunnel stack on a Kubernetes cluster. Both programmatic-access and manual flows. ## Use the tunnel in an agent Once your stack is running and has one or more MCP servers configured, attach an upstream MCP server to a Managed Agent session. To call the same servers from the Messages API instead, see [Use the tunneled MCP servers](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/overview#use-the-tunneled-mcp-servers). The picker only shows tunnels with at least one active certificate. A tunnel that still shows **Needs certificate** in the **MCP tunnels** list does not appear in the dropdown; register a CA certificate first. The picker is also workspace-scoped: it lists tunnels in the same workspace as the session, not other workspaces. Go to **Managed Agents > Sessions** and click **New session**. In the agent picker, choose **Create new agent** so you can edit the MCP server list directly. Click **+ MCP Server** and open the dropdown. Tunnels created in the current workspace appear at the top of the list, above the public connector catalog. Select the tunnel that fronts the server you want to reach. The card shows two optional fields: **Subdomain** (prefixed to the tunnel domain) and **Path** (appended after it). Fill in one or both, depending on how your proxy's routes are configured. The **Resolves to** line shows the full MCP server URL that the agent connects to. The tunnel carries traffic; it does not authenticate to the upstream MCP server. Configure OAuth or bearer auth on the MCP server the same way as for any other MCP server. ## Archive a tunnel Archiving immediately stops the tunnel from accepting connections and is permanent. In the **MCP tunnels** list, open the row menu for the tunnel and choose **Archive**. Archived tunnels remain visible when you filter the list by **Archived** or **All**. ## Next steps Install on a Kubernetes cluster using the Anthropic Helm chart. Hardening guidance, credential rotation, and breach response. --- title: MCP tunnels reference url: https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/reference description: Proxy configuration fields, the Tunnels REST API, certificate requirements, and the setup component. --- MCP tunnels are in research preview. [Request access](https://claude.com/form/claude-managed-agents) to try them. ## Proxy configuration The [proxy](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#components) reads its configuration from `/etc/mcp-gateway/config.yaml` (Compose) or the rendered ConfigMap (Helm, populated from `gateway.config.*`). | Field | Description | Default | | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | | `listen_addr` | Address and port to listen on. | Required | | `log_level` | Logging verbosity: `debug`, `info`, `warn`, or `error`. | `info` | | `shutdown_timeout` | How long to wait for in-flight requests during graceful shutdown. | `30s` | | `tunnel_domain` | Base domain assigned to the tunnel. When set, route lookup strips this suffix from incoming hostnames so `routes` keys can be bare subdomains (`wiki`). When empty, `routes` keys must be exact full hostnames. | Required when `routes` keys are bare subdomains | | `tls.cert_file` | Path to the server TLS certificate. | Required | | `tls.key_file` | Path to the server TLS private key. | Required | | `routes` | Map of subdomain or full hostname to upstream URL. See [Route matching](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/reference#route-matching). | Required | | `upstream.allowed_ips` | IPv4 CIDR ranges or single addresses the proxy is permitted to connect to. Mutually exclusive with `disable_ip_validation`. | RFC1918 private ranges | | `upstream.disable_ip_validation` | Disable upstream IP validation entirely. Mutually exclusive with `allowed_ips`. | `false` | | `upstream.tls.ca_file` | CA bundle for validating upstream TLS. | None | | `upstream.tls.include_system_cas` | Also trust the system CA bundle for upstream TLS. | `false` | For `https://` upstream routes, set at least one of `upstream.tls.ca_file` or `upstream.tls.include_system_cas`; otherwise the proxy has no trust anchor for the upstream certificate. ### Route matching `routes` is a flat string map (`map[string]string`), not a list. The proxy looks up the incoming hostname by exact match first, then by stripping the `tunnel_domain` suffix and matching the remaining subdomain. The match considers only the hostname; the request path and query string are forwarded to the [upstream MCP server](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#components) unchanged. Each upstream value must be exactly `scheme://host:port`. The port is mandatory. Including a path is rejected at config load with `invalid upstream (must be scheme://host:port)`. ## Tunnels API The Tunnels REST API lives at `/v1/tunnels` and supports creating, listing, and archiving tunnels, registering CA certificates, and revealing or rotating the tunnel token. See the [Tunnels API reference](https://platform.claude.com/docs/en/api/beta/tunnels/list) for all endpoints, request and response schemas, and examples. The previous Admin API surface at `/v1/organizations/tunnels` (beta header `mcp-tunnels-2026-05-19`, scope `org:manage_tunnels`) continues to work during a migration window and remains documented in the [Admin API reference](https://platform.claude.com/docs/en/api/admin/mcp_tunnels) with a deprecation notice. To migrate, update the path to `/v1/tunnels`, the beta header to `mcp-tunnels-2026-06-22`, and your WIF token scope to `workspace:manage_tunnels`. All MCP tunnels endpoints require a bearer token with the `workspace:manage_tunnels` scope obtained through [Workload Identity Federation](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation). Admin API keys are not accepted. Required headers on every request: | Header | Value | | ------------------- | ------------------------------------------ | | `Authorization` | `Bearer ` (the WIF-exchanged token) | | `anthropic-version` | `2023-06-01` | | `anthropic-beta` | `mcp-tunnels-2026-06-22` | ## Certificate requirements The [setup component](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#components) generates compliant certificates automatically. These requirements apply only if you issue certificates through your own PKI. ### CA certificate Upload with `POST /v1/tunnels/{tunnel_id}/certificates`. A tunnel can hold up to two active CA certificates at a time, which allows zero-downtime rotation. * PEM-encoded, single certificate, up to 8 kB. * `BasicConstraints` extension present with `CA:TRUE`, marked critical. * `SubjectKeyIdentifier` extension present. * `KeyUsage` includes `keyCertSign`. * Within its validity period. * RSA 2048-bit or larger, or ECDSA P-256 or larger, with a SHA-256 or stronger signature. ### Server certificate Presented by the proxy during [inner TLS](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#components). * Signed directly by a registered CA (no intermediates). * `AuthorityKeyIdentifier` extension present and matching the CA's `SubjectKeyIdentifier`. * Subject Alternative Name includes a DNS name matching `.`. A wildcard `*.` covers all routes. * If the `ExtendedKeyUsage` extension is present, it includes `serverAuth`. * Within its validity period. * RSA 2048-bit or larger, or ECDSA P-256 or larger, with a SHA-256 or stronger signature. The setup component generates an ECDSA P-256 CA with five-year validity and an RSA 4096-bit server certificate with a wildcard SAN and 90-day validity. ## Setup component The setup component ships inside the `mcp-proxy` image as the `setup` binary. Run it with `docker compose run --rm setup ` (Compose) or rely on the chart's hooks and CronJobs (Helm). ### `setup init` Attaches to an existing tunnel (or creates one when no tunnel ID is supplied), then generates a CA and server certificate, registers the CA, retrieves the tunnel token, and writes all outputs to the destination. | Flag | Description | Default | | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | | `--api-url` | Claude API base URL. Also read from `API_URL`. | Required | | `--tunnel-id` | Tunnel ID to attach to (`tnl_...`). Also read from `TUNNEL_ID`. When omitted, a new tunnel is created; a tunnel ID already stored in the output is reused on re-runs. | None (create a tunnel) | | `--output` | Output destination: `dir:/path` or `k8s-secret:NAME`. The Helm chart passes `k8s-secret:`. | `k8s-secret:mcp-tunnel` (auto-detected when running in a Kubernetes pod; required otherwise) | | `--cert-duration` | Server certificate validity period. | `2160h` (90 days) | | `--token-version` | Change-detection string. A new value triggers token rotation on re-run. The Helm chart and the Compose example both pass `1` as the initial value. | None | The command authenticates through [Workload Identity Federation](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation). It reads `ANTHROPIC_FEDERATION_RULE_ID`, `ANTHROPIC_ORGANIZATION_ID`, `ANTHROPIC_WORKSPACE_ID` (optional), and exactly one of `ANTHROPIC_IDENTITY_TOKEN_FILE` or `ANTHROPIC_IDENTITY_TOKEN`. See the [WIF reference](https://platform.claude.com/docs/en/manage-claude/wif-reference) for the current semantics of these variables; the setup component derives the service account from the federation rule, so it does not require `ANTHROPIC_SERVICE_ACCOUNT_ID` separately. ### `setup renew-cert` Issues a new server certificate signed by the stored CA. Makes no API calls. | Flag | Description | Default | | ----------------- | --------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | | `--output` | Output destination: `dir:/path` or `k8s-secret:NAME`. The Helm chart passes `k8s-secret:`. | `k8s-secret:mcp-tunnel` (auto-detected when running in a Kubernetes pod; required otherwise) | | `--cert-duration` | New certificate validity period. | `2160h` (90 days) | | `--renew-before` | Skip renewal if the existing certificate has more than this duration remaining. | `0` (always renew) | Setting `--renew-before=720h` makes the command a no-op when more than 30 days of validity remain, so it's safe to run on a fixed schedule. --- title: MCP tunnels security url: https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/security description: Hardening guidance, credential rotation, breach response, and teardown for MCP tunnel deployments. --- MCP tunnels are in research preview. [Request access](https://claude.com/form/claude-managed-agents) to try them. The tunnel architecture provides strong defaults (outbound-only connectivity, end-to-end encryption, and IP validation), but the overall security of your [tunnel stack](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#components) also depends on how you configure and operate it. This page covers recommended hardening, breach response, and how to decommission a tunnel. ## Best practices * **Require OAuth on every MCP server.** Configure each [upstream MCP server](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#components) to require OAuth as described in the [MCP authorization spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization). OAuth provides defense in depth on top of the tunnel's transport authentication and enables user-level authorization at the data layer. * **Enable SSO for your organization.** Tunnels, federation rules, and service accounts are managed in the Claude Console. SSO enforces your identity provider's session controls on the admins who can change them. * **Restrict `upstream.allowed_ips`.** Use the smallest CIDR ranges that cover your MCP servers. This is the [proxy](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#components)'s primary SSRF defense. * **Monitor logs.** Alert on warnings, errors, and unusual traffic patterns from the tunnel stack. * **Rotate credentials.** Rotate the server certificate and tunnel token on a regular schedule, and immediately if you suspect compromise. * **Keep images updated.** Track new proxy releases and pin images by SHA-256 digest. * **Limit network reach.** The proxy and [cloudflared](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#components) should only be able to reach the destinations listed in the [network requirements](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/overview#network-requirements). Use NetworkPolicy (Kubernetes) or host firewall rules (Compose). * **Limit MCP server scope.** Each server should expose only the tools and data required for its purpose. * **Protect credentials at rest.** Apply your organization's secrets-management practices to private keys and tunnel tokens. ## Respond to a suspected breach If you believe your tunnel token, TLS keys, or proxy host has been compromised: ```bash helm uninstall mcp-tunnel -n mcp-tunnel ``` ```bash docker compose down --timeout 0 ``` Remove the upstream MCP servers from any Managed Agent sessions that use them, and stop passing their URLs in the `mcp_servers` block of Messages API requests. Archiving invalidates the tunnel token and detaches the domain. In the Console, [archive the tunnel](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/console#archive-a-tunnel) from the **MCP tunnels** list. To archive over the API instead, see [Archive a tunnel](https://platform.claude.com/docs/en/api/beta/tunnels/archive). Report the suspected compromise to Anthropic support. Re-provision a fresh tunnel and rotate any OAuth tokens that the affected MCP servers issued. Inspect proxy, cloudflared, and MCP server logs for the window of suspected compromise before bringing the new tunnel online. ## Tear down a tunnel Follow these steps to decommission a tunnel and remove all stored credentials. ```bash helm uninstall mcp-tunnel -n mcp-tunnel ``` ```bash docker compose down ``` In the Console, [archive the tunnel](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/console#archive-a-tunnel) from the **MCP tunnels** list. With programmatic access, the setup component created a single Secret named after the release. Without programmatic access, you created `mcp-tunnel-token` and `mcp-tunnel-cert` yourself. Delete whichever apply: ```bash kubectl -n mcp-tunnel delete secret \ mcp-tunnel mcp-tunnel-token mcp-tunnel-cert \ --ignore-not-found ``` Private keys and certificates live in `data/`. The tunnel token lives in `data/tunnel-token` (programmatic flow) or in your shell environment (manual flow). The `config/` directory and `docker-compose.yaml` contain no secrets; keep them if you plan to re-provision, or remove them as well. ```bash sudo rm -rf data ``` --- title: Troubleshoot MCP tunnels url: https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/troubleshooting description: Diagnose connectivity, TLS, IP validation, and OAuth routing issues in a tunnel stack. --- MCP tunnels are in research preview. [Request access](https://claude.com/form/claude-managed-agents) to try them. A request through the tunnel can fail at one of three layers; diagnose them in order: the outbound connection to the [tunnel edge](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#components), the [inner TLS](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#components) from Anthropic to your [proxy](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#components), then routing and IP validation toward the [upstream MCP server](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#components). ## Quick reference | Symptom | Cause | Fix | | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Tunnel doesn't appear in the agent **+ MCP Server** picker | The picker only lists tunnels in the session's workspace that have at least one active certificate. | Register a CA certificate, or open the session in the workspace the tunnel was created in. | | Caller sees HTTP 500; [cloudflared](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#components) logs `No ingress rules were defined` | cloudflared has no local target. | Add `--url http://localhost:8080` and `network_mode: "service:mcp-proxy"` to the cloudflared service. | | Proxy logs `no route for host` | `tunnel_domain` doesn't match the assigned domain, or `config.yaml` was edited without restarting. | Set `tunnel_domain` to the exact domain shown on the tunnel detail page, then restart the proxy (`docker compose restart mcp-proxy`). | | Proxy logs `IP validation failed: is not a private address` | Upstream MCP server resolves outside RFC1918. | See [Upstream IP validation](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/troubleshooting#upstream-ip-validation). | | Proxy exits with `cannot unmarshal !!seq into map[string]string` | `routes` is a YAML list. | Use `routes: { name: http://host:port }`. | | Proxy exits with `open /data/tls.key: permission denied` | The key is `0600`; the proxy container runs non-root. | `chmod 644 data/tls.key`. | | `curl https://:8080` fails with `wrong version number` | Expected; the listener is plaintext WebSocket. TLS happens inside the WS stream. | Verify through a [Managed Agent or the Messages API](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/overview#use-the-tunneled-mcp-servers) instead. | The following sections cover failures that need more than a one-line fix. ## OAuth fails behind a source-IP allowlist OAuth flows fail when your authorization server's source-IP allowlist blocks Anthropic's backend from reaching `/token`, `/register`, and the discovery endpoints. If you'd rather not allowlist Anthropic's egress ranges, you can route the backend-to-backend OAuth calls through the tunnel while keeping the browser-facing `/authorize` endpoint on your existing public hostname. ```yaml routes: mcp: http://your-mcp-server:8080 auth: http://your-auth-server:8080 ``` Restart the proxy after editing `routes` (`docker compose restart mcp-proxy`, or `helm upgrade`). Your authorization server's `/.well-known/oauth-authorization-server` response should point `authorization_endpoint` at your existing allowlisted hostname and everything else at the tunnel: ```json { "issuer": "https://auth.", "authorization_endpoint": "https:///authorize", "token_endpoint": "https://auth./token", "registration_endpoint": "https://auth./register", "code_challenge_methods_supported": ["S256"] } ``` Your MCP server's `/.well-known/oauth-protected-resource` response should reference the tunnel hostname as its authorization server: ```json { "resource": "https://mcp.", "authorization_servers": ["https://auth."] } ``` With this configuration, the user's browser hits `/authorize` on your existing hostname (which your allowlist already permits), while Anthropic's backend reaches `/token`, `/register`, and the discovery documents through the tunnel. ## Setup component authentication failures The [setup component](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/concepts#components) (Helm Job or Compose `setup` service) authenticates to the Tunnels API by exchanging an OIDC JWT through your federation rule. When the exchange fails, see [Troubleshoot a failed exchange](https://platform.claude.com/docs/en/manage-claude/wif-reference#troubleshoot-a-failed-exchange) in the Workload Identity Federation reference; the failure modes (subject, audience, issuer, JWKS, lifetime) are the same. Tunnels-specific causes: * The chart's default audience is `api.anthropic.com` (no scheme). If your rule's audience is `https://api.anthropic.com`, set `api.wif.audience` to match. * A `403` from the Tunnels API after a successful exchange means the rule's scope doesn't include `workspace:manage_tunnels`, or the rule's service account isn't a member of the tunnel's workspace. Set the scope and add the service account to the workspace. On Helm, the setup component runs as a pre-install hook Job. On failure, the Job is left behind for inspection (`kubectl logs job/mcp-tunnel-setup -n mcp-tunnel`). Helm doesn't manage hook resources, so delete it before retrying: ```bash helm uninstall mcp-tunnel -n mcp-tunnel kubectl -n mcp-tunnel delete job mcp-tunnel-setup ``` ## Tunnel won't connect Check the cloudflared logs first. Common causes: * The `TUNNEL_TOKEN` is missing, expired, or copied incorrectly. * A firewall is blocking outbound TCP/UDP on port 7844 to the tunnel edge. cloudflared may also log warnings about UDP receive buffer sizes; this is a QUIC tuning hint, not an error. ## Certificate errors When Anthropic rejects the proxy's certificate during inner TLS, the proxy logs `tls handshake failed`. Verify that: * The server certificate has not expired. * The certificate's Subject Alternative Name matches `*.`. * The signing CA is registered with Anthropic for this tunnel. See the [certificate requirements](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/reference#certificate-requirements) for the full validation rules. ## Upstream IP validation For SSRF protection, the proxy only dials addresses in the RFC1918 private ranges (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`) by default. Only IPv4 is supported for the proxy-to-upstream connection. (The cloudflared-to-edge egress range in [Network requirements](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/overview#network-requirements) is a different hop.) If the proxy logs `IP validation failed: is not a private address`, the upstream hostname resolved outside that set. On Kubernetes, some managed distributions allocate the Service CIDR outside RFC1918; if `kubectl get svc kubernetes -n default -o jsonpath='{.spec.clusterIP}'` returns an address outside the private ranges, look up your cluster's Service CIDR and add it. If the address is legitimate, add the narrowest covering CIDR to `upstream.allowed_ips`. Setting `allowed_ips` **replaces** the RFC1918 default rather than extending it, so include the private ranges your other upstream MCP servers use: ```yaml config/mcp-proxy.yaml upstream: allowed_ips: - 10.0.0.0/8 - 172.16.0.0/12 - 192.168.0.0/16 - 127.0.0.0/8 # loopback, for local testing only ``` Avoid `0.0.0.0/0` outside of local testing; it disables SSRF protection entirely. ### Claude on cloud platforms --- title: Claude in Amazon Bedrock (Opus 4.7 and later) url: https://platform.claude.com/docs/en/build-with-claude/claude-in-amazon-bedrock description: Access Claude models through Amazon Bedrock with AWS-native authentication, billing, and security boundaries. --- This guide walks you through setting up and making API calls to Claude in Amazon Bedrock. Claude in Amazon Bedrock runs on AWS-managed infrastructure with zero operator access (Anthropic personnel have no access to the inference infrastructure), letting you build sensitive applications entirely inside the AWS security boundary while using the same Messages API shape you use with Anthropic's first-party API. This page covers Claude in Amazon Bedrock, which serves Claude through the Messages API at `/anthropic/v1/messages` on AWS-managed infrastructure. The previous Amazon Bedrock integration (the `InvokeModel` and `Converse` APIs with ARN-versioned model identifiers) remains available and is documented at [Claude on Amazon Bedrock (Opus 4.6 and earlier)](https://platform.claude.com/docs/en/build-with-claude/claude-on-amazon-bedrock-legacy). For an Anthropic-operated alternative on AWS with AWS Marketplace billing and typically same-day feature access, see [Claude Platform on AWS](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws). ## Access Amazon Bedrock sets access criteria for each Claude model individually. Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5, Claude Opus 4.7, and Claude Haiku 4.5 are open to all Amazon Bedrock customers; for any other model's current criteria, check [Amazon Bedrock model access](https://console.aws.amazon.com/bedrock/home#/modelaccess) in the AWS console. Claude Mythos Preview requires an invitation; see [Project Glasswing](https://anthropic.com/glasswing). For region availability, see [Regions](https://platform.claude.com/docs/en/build-with-claude/claude-in-amazon-bedrock#regions). ## Prerequisites Before you begin, ensure you have: * An AWS account with [Amazon Bedrock model access](https://console.aws.amazon.com/bedrock/home#/modelaccess) enabled for the Claude models you intend to use. * The [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) installed and configured (optional, for credential management). Claude Mythos Preview additionally requires a dedicated AWS account that has been allowlisted by the Bedrock Marketplace team. Your Anthropic account executive can submit your account ID for allowlisting (typically processed within 24 hours), and AWS sends a welcome email once it's complete. ## Authentication Claude in Amazon Bedrock supports three authentication paths. Choose the one that best fits your security requirements. ### Bedrock service role (recommended) Use a Bedrock service role with AWS-managed keys for the most secure, long-lived access: An AWS administrator provisions a Bedrock service role and grants developers `iam:PassRole` permission on the service role ARN. When calling the API, Bedrock assumes the service role on your behalf. See the [Amazon Bedrock documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/bedrock-mantle.html) for how to associate the role with your requests. ### IAM assumed roles For identity-federated access with a 12-hour maximum session: Create an IAM role scoped to your Claude models. The trust policy names your identity provider (SAML, OIDC, or AWS Identity Center). The permissions policy grants `bedrock-mantle:CreateInference` only on the allowed model ARNs. Authenticate through your corporate identity provider, then assume the IAM role. AWS STS issues temporary credentials that the SDK or CLI uses to sign requests. ### Bearer tokens For short-term access without IAM roles (12-hour maximum, least preferred): Block long-term keys by attaching a policy that denies `bedrock:CallWithBearerToken` unless the `bedrock:BearerTokenType` condition matches a short-term token. Use the `aws-bedrock-token-generator` CLI to mint a bearer token. Pass it in the `x-api-key` header on each request. ## Install an SDK Anthropic's [client SDKs](https://platform.claude.com/docs/en/cli-sdks-libraries/overview) support Claude in Amazon Bedrock through a Bedrock-specific package or module. ```bash pip install -U "anthropic[bedrock]" ``` ```bash npm install @anthropic-ai/bedrock-sdk ``` ```bash dotnet add package Anthropic.Bedrock ``` ```bash go get github.com/anthropics/anthropic-sdk-go/bedrock ``` ```kotlin implementation("com.anthropic:anthropic-java-bedrock:2.53.0") ``` ```xml com.anthropic anthropic-java-bedrock 2.53.0 ``` ```bash composer require anthropic-ai/sdk aws/aws-sdk-php ``` ```bash # Gemfile gem "anthropic" gem "aws-sdk-core" ``` ## Making your first request The endpoint follows the pattern `https://bedrock-mantle.{region}.api.aws/anthropic/v1/messages`. Unlike the `InvokeModel`-based integration, this endpoint uses standard SSE streaming and the same request body shape as Anthropic's first-party API. The SDK resolves credentials and region using the standard AWS precedence: constructor arguments, then environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`, `AWS_REGION`), then the AWS config file and credential chain (SSO, assumed roles, ECS task role, IMDS). ```bash curl https://bedrock-mantle.us-east-1.api.aws/anthropic/v1/messages \ --aws-sigv4 "aws:amz:us-east-1:bedrock-mantle" \ --user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" \ -H "x-amz-security-token: $AWS_SESSION_TOKEN" \ -H "content-type: application/json" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "anthropic.claude-opus-5", "max_tokens": 1024, "messages": [ {"role": "user", "content": "Hello, Claude"} ] }' ``` The `ant` CLI does not support Amazon Bedrock. Use either cURL or an SDK. ```python from anthropic import AnthropicBedrockMantle client = AnthropicBedrockMantle(aws_region="us-east-1") message = client.messages.create( model="anthropic.claude-opus-5", max_tokens=1024, messages=[{"role": "user", "content": "Hello, Claude"}], ) print(next(block.text for block in message.content if block.type == "text")) ``` ```typescript import { AnthropicBedrockMantle } from "@anthropic-ai/bedrock-sdk"; const client = new AnthropicBedrockMantle({ awsRegion: "us-east-1" }); const message = await client.messages.create({ model: "anthropic.claude-opus-5", max_tokens: 1024, messages: [{ role: "user", content: "Hello, Claude" }] }); const textBlock = message.content.find((block) => block.type === "text"); if (textBlock) { console.log(textBlock.text); } ``` ```csharp using Anthropic.Bedrock; using Anthropic.Models.Messages; var client = new AnthropicBedrockMantleClient(new() { AwsRegion = "us-east-1" }); var message = await client.Messages.Create(new() { Model = "anthropic.claude-opus-5", MaxTokens = 1024, Messages = [new() { Role = Role.User, Content = "Hello, Claude" }], }); foreach (var item in message.Content) { if (item.Value is TextBlock block) { Console.WriteLine(block.Text); break; } } ``` ```go client, err := bedrock.NewMantleClient(context.Background(), bedrock.MantleClientConfig{ AWSRegion: "us-east-1", }) if err != nil { panic(err) } message, err := client.Messages.New(context.Background(), anthropic.MessageNewParams{ Model: "anthropic.claude-opus-5", MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Hello, Claude")), }, }) if err != nil { panic(err) } for _, block := range message.Content { if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok { fmt.Println(textBlock.Text) break } } ``` ```java import com.anthropic.bedrock.backends.BedrockMantleBackend; import com.anthropic.client.AnthropicClient; import com.anthropic.client.okhttp.AnthropicOkHttpClient; import com.anthropic.models.messages.ContentBlock; import com.anthropic.models.messages.Message; import com.anthropic.models.messages.MessageCreateParams; void main() { AnthropicClient client = AnthropicOkHttpClient.builder() .backend(BedrockMantleBackend.fromEnv()) .build(); Message message = client.messages().create( MessageCreateParams.builder() .model("anthropic.claude-opus-5") .maxTokens(1024) .addUserMessage("Hello, Claude") .build() ); message.content().stream() .filter(ContentBlock::isText) .findFirst() .ifPresent(block -> IO.println(block.asText().text())); } ``` ```php use Anthropic\Bedrock\MantleClient; $client = new MantleClient(awsRegion: 'us-east-1'); $message = $client->messages->create( model: 'anthropic.claude-opus-5', maxTokens: 1024, messages: [ ['role' => 'user', 'content' => 'Hello, Claude'], ], ); echo array_find($message->content, fn ($block) => $block->type === 'text')->text; ``` ```ruby require "anthropic" client = Anthropic::BedrockMantleClient.new(aws_region: "us-east-1") message = client.messages.create( model: "anthropic.claude-opus-5", max_tokens: 1024, messages: [{role: "user", content: "Hello, Claude"}] ) puts message.content.find { it.type == :text }.text ``` You can also use the standard `Anthropic` client: set `base_url` to `https://bedrock-mantle.{region}.api.aws/anthropic` and pass your bearer token as `api_key`. This path supports bearer-token authentication only. SigV4 signing requires the dedicated client. ## Supported models Model IDs in Claude in Amazon Bedrock carry an `anthropic.` provider prefix. Model capabilities and behaviors are documented on the [Models overview](https://platform.claude.com/docs/en/about-claude/models/overview) page. | Model | Model ID | Access | | --------------------- | ------------------------------- | --------------------------------------------------------------------------------------------------- | | Claude Fable 5 | anthropic.claude-fable-5 | Open | | Claude Opus 5 | anthropic.claude-opus-5 | See [Access](https://platform.claude.com/docs/en/build-with-claude/claude-in-amazon-bedrock#access) | | Claude Opus 4.8 | anthropic.claude-opus-4-8 | Open | | Claude Opus 4.7 | anthropic.claude-opus-4-7 | Open | | Claude Sonnet 5 | `anthropic.claude-sonnet-5` | Open | | Claude Haiku 4.5 | anthropic.claude-haiku-4-5 | Open | | Claude Mythos Preview | anthropic.claude-mythos-preview | Invitation only ([Project Glasswing](https://anthropic.com/glasswing)) | Upgrading to a newer Claude model? In Claude Code, run `/claude-api migrate` to apply model ID swaps and breaking parameter changes across your codebase. The skill detects which cloud platform your code targets and adjusts model ID formats and feature changes for that platform. See [Migrating to a newer Claude model](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/claude-api-skill#migrating-to-a-newer-claude-model). ## Feature support For the full feature list with Amazon Bedrock availability, see [Features overview](https://platform.claude.com/docs/en/build-with-claude/overview). ### Supported feature highlights * [Messages API](https://platform.claude.com/docs/en/api/messages/create) (`/anthropic/v1/messages`) * [Prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) * [Thinking](https://platform.claude.com/docs/en/build-with-claude/thinking) * [Tool use](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview), including the [Bash tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/bash-tool), [Computer use tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/computer-use-tool), [Memory tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/memory-tool), and [Text editor tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/text-editor-tool) * [Citations](https://platform.claude.com/docs/en/build-with-claude/citations) ### Features not supported * [Structured outputs](https://platform.claude.com/docs/en/build-with-claude/structured-outputs) * Input sources (URL sources for images and documents, Files API) * Server-side tools (code execution, web search, web fetch, advisor) * Agent infrastructure (Agent Skills, MCP connector, programmatic tool calling) * API endpoints (Message Batches, Models, Admin, Compliance, Usage and Cost) * Claude Managed Agents * Server-side fallback (the [`fallbacks` parameter](https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback#server-side-fallback); use the [client-side fallback pattern](https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback#client-side-fallback) instead) ## Regions Claude in Amazon Bedrock is available in the following AWS regions. Amazon Bedrock offers two endpoint types: * **Global:** dynamic routing across all available regions for maximum availability. No pricing premium. * **Regional:** the endpoint resolves to the single AWS region you specify, for data-residency requirements. Regional endpoints carry a 10% pricing premium over global endpoints. To route across multiple regions within a geography, use an [inference profile](https://docs.aws.amazon.com/bedrock/latest/userguide/cross-region-inference.html) (US, EU, JP, or AU). Regions marked **In-region only** in the table support direct single-region routing without an inference profile. The global endpoint is available for Claude Fable 5, Claude Opus 5, Claude Opus 4.8, Claude Opus 4.7, Claude Sonnet 5, and Claude Haiku 4.5. Claude Mythos Preview is regional only and is available in `us-east-1`. | AWS region | Location | Endpoint types | | ---------------- | ------------------------- | -------------------------- | | `af-south-1` | Africa (Cape Town) | Global | | `ap-northeast-1` | Asia Pacific (Tokyo) | Global, JP, In-region only | | `ap-northeast-2` | Asia Pacific (Seoul) | Global | | `ap-northeast-3` | Asia Pacific (Osaka) | Global, JP | | `ap-south-1` | Asia Pacific (Mumbai) | Global | | `ap-south-2` | Asia Pacific (Hyderabad) | Global | | `ap-southeast-1` | Asia Pacific (Singapore) | Global | | `ap-southeast-2` | Asia Pacific (Sydney) | Global, AU | | `ap-southeast-3` | Asia Pacific (Jakarta) | Global | | `ap-southeast-4` | Asia Pacific (Melbourne) | Global, AU, In-region only | | `ca-central-1` | Canada (Central) | Global, US | | `ca-west-1` | Canada West (Calgary) | Global | | `eu-central-1` | Europe (Frankfurt) | Global, EU | | `eu-central-2` | Europe (Zurich) | Global, EU | | `eu-north-1` | Europe (Stockholm) | Global, EU, In-region only | | `eu-south-1` | Europe (Milan) | Global, EU | | `eu-south-2` | Europe (Spain) | Global, EU | | `eu-west-1` | Europe (Ireland) | Global, EU, In-region only | | `eu-west-2` | Europe (London) | Global, EU | | `eu-west-3` | Europe (Paris) | Global, EU | | `il-central-1` | Israel (Tel Aviv) | Global | | `me-central-1` | Middle East (UAE) | Global | | `sa-east-1` | South America (São Paulo) | Global | | `us-east-1` | US East (N. Virginia) | Global, US, In-region only | | `us-east-2` | US East (Ohio) | Global, US, In-region only | | `us-west-1` | US West (N. California) | Global, US | | `us-west-2` | US West (Oregon) | Global, US, In-region only | ## Quotas Default quota is 2 million input tokens per minute (TPM). You can request up to 4 million input TPM without additional Anthropic approval. AWS enforces requests-per-minute (RPM) limits on the Bedrock side; contact AWS support for RPM adjustments. ## Data retention Data handling for this offering is governed by Amazon Bedrock. For details, see [Data protection in Amazon Bedrock](https://docs.aws.amazon.com/bedrock/latest/userguide/data-protection.html). ## Monitoring and logging Claude in Amazon Bedrock emits logs to both CloudWatch and CloudTrail. Anthropic recommends retaining activity logs on at least a 30-day rolling basis to understand usage patterns and investigate potential issues. ## Support For support, contact **[bedrock-ant-eap@amazon.com](mailto:bedrock-ant-eap@amazon.com)**. Include your AWS account ID and the `request-id` from any failed API responses. **Claude Mythos Preview** is a research preview model available to invited customers on Amazon Bedrock. For more information, see [Project Glasswing](https://anthropic.com/glasswing). --- title: Claude in Microsoft Foundry url: https://platform.claude.com/docs/en/build-with-claude/claude-in-microsoft-foundry description: Access Claude models through Microsoft Foundry with Azure-native endpoints and authentication. --- This guide shows you how to set up and make API calls to Claude in Microsoft Foundry using one of Anthropic's client SDKs or direct HTTP requests. When you access Claude in Microsoft Foundry, you are billed for Claude usage in the Azure Marketplace. You can use the latest Claude models, including Claude Opus 5, Claude Opus 4.8, and Claude Sonnet 5, and features such as the [1M-token context window](https://platform.claude.com/docs/en/build-with-claude/context-windows), while managing costs through your Azure subscription. Claude is available in Global Standard and US Data Zone Standard deployment types in Foundry resources, billed in Claude Consumption Units through the Azure Marketplace. Visit [Claude in Microsoft Foundry pricing](https://platform.claude.com/docs/en/about-claude/pricing#claude-in-microsoft-foundry-pricing) for details. ## Hosting options Claude models in Microsoft Foundry are available in two hosting options. You choose the hosting option when you configure the deployment. | | Hosted on Azure | Hosted on Anthropic | | -------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Where inference runs | Anthropic-operated service running on Azure infrastructure | Anthropic-operated service running on Anthropic infrastructure | | Model availability | The latest models in the Opus, Sonnet, and Haiku families | All Claude models available on Microsoft Foundry | | Deployment types | Global Standard, US Data Zone Standard | Global Standard | | Recommended for | Most workloads | [Access to features or models not yet hosted on Azure](https://platform.claude.com/docs/en/build-with-claude/claude-in-microsoft-foundry#additional-features-not-supported-when-hosted-on-azure) | Anthropic acts as an independent processor for Microsoft. Customers using Claude through Microsoft Foundry are subject to Anthropic's data use terms. For deployments hosted on Azure, prompts and completions remain within Azure. Only usage metadata and content flagged by Anthropic's safety systems egress to Anthropic. Anthropic continues to provide its safety and data commitments. ## Prerequisites Before you begin, ensure you have: * An active Azure subscription * Access to the [Foundry portal](https://ai.azure.com/) * The [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) installed (required for the Entra ID cURL example, optional otherwise) * An Azure RBAC role that allows you to use the resource, such as **Foundry User** (formerly Azure AI User) or **Cognitive Services User** ## Install an SDK Anthropic's [client SDKs](https://platform.claude.com/docs/en/cli-sdks-libraries/overview) support Foundry through a platform-specific package or client class. The examples on this page also show requests with cURL and the ant CLI. To set up the CLI, see [CLI quickstart](https://platform.claude.com/docs/en/cli-sdks-libraries/cli/quickstart). Foundry is supported by the C#, Java, PHP, Python, and TypeScript SDKs. Foundry is not currently available in the Go and Ruby SDKs. ```bash pip install -U "anthropic" # For Entra ID authentication, also install the Azure Identity library pip install azure-identity ``` ```bash npm install @anthropic-ai/foundry-sdk # For Entra ID authentication, also install the Azure Identity library npm install @azure/identity ``` ```bash dotnet add package Anthropic.Foundry ``` ```bash # The Go SDK does not yet support Foundry natively (see the Authentication # examples for using the standard Go SDK as a workaround) go get github.com/anthropics/anthropic-sdk-go ``` ```kotlin implementation("com.anthropic:anthropic-java-foundry:2.53.0") // For Entra ID authentication, also add the Azure Identity library implementation("com.azure:azure-identity:1.18.3") ``` ```xml com.anthropic anthropic-java-foundry 2.53.0 com.azure azure-identity 1.18.3 ``` ```bash composer require "anthropic-ai/sdk" "guzzlehttp/guzzle:^7" ``` ```bash # The Ruby SDK does not yet support Foundry natively (see the Authentication # examples for using the standard Ruby SDK as a workaround) # Gemfile gem "anthropic" ``` ## Provisioning Foundry uses a two-level hierarchy: **resources** contain your security and billing configuration, while **deployments** are the model instances you call through the API. You'll first create a Foundry resource, then create one or more Claude deployments within it. ### Provisioning Foundry resources Create a Foundry resource, which is required to use and manage services in Azure. You can follow these instructions to create a [Foundry resource](https://learn.microsoft.com/en-us/azure/ai-services/multi-service-resource?pivots=azportal#create-a-new-azure-ai-foundry-resource). Alternatively, you can start by creating a [Foundry project](https://learn.microsoft.com/en-us/azure/foundry/how-to/create-projects), which involves creating a Foundry resource. To provision your resource: 1. Navigate to the [Foundry portal](https://ai.azure.com/). 2. Create a new Foundry resource or select an existing one. 3. Configure access management using Azure-issued API keys or Entra ID (formerly Azure Active Directory) for role-based access control. 4. Optionally configure the resource to be part of a private network (Azure Virtual Network) to restrict network access to your resource. 5. Note your resource name. You'll use this as `{resource}` in API endpoints (for example, `https://{resource}.services.ai.azure.com/anthropic/v1/*`). ### Creating Foundry deployments After creating your resource, deploy a Claude model to make it available for API calls. These steps describe the new Foundry portal (the **New Foundry** toggle is on): 1. Sign in to the Foundry portal. From the portal homepage, select **Discover** in the upper-right navigation, then **Models** in the left pane to open the model catalog. 2. Search for and select a Claude model (for example, claude-opus-5). Each model appears once in the catalog regardless of how many hosting options it supports. 3. On the model card, select **Deploy**, then **Custom settings** to open the deployment settings pane. If you choose **Default settings** instead, the deployment is automatically configured as Hosted on Azure for models available in both hosting options. 4. On your first Claude deployment, review the Azure Marketplace terms, select an industry, and select **Agree and Proceed** to accept the terms and subscribe to the Azure Marketplace offer. 5. Configure the deployment: * **Deployment name:** Defaults to the model ID, but you can customize it (for example, `my-claude-deployment`). The deployment name cannot be changed after creation. * **Region scope:** Select Global, or for models hosted on Azure, Data Zone. Selecting Data Zone creates a US Data Zone Standard deployment, which keeps inference within the United States and is equivalent to setting [`inference_geo: "us"`](https://platform.claude.com/docs/en/manage-claude/data-residency#inference-geo) on the Claude API. * **Model version:** Expand **Model version settings** and select a version from the **Model version** dropdown menu. Each [hosting option](https://platform.claude.com/docs/en/build-with-claude/claude-in-microsoft-foundry#hosting-options) is listed as a separate model version, labeled with its hosting option (for example, version 1 for Hosted on Anthropic, version 2 for Hosted on Azure). 6. Select **Deploy** and wait for provisioning to complete. 7. Once deployed, select **Build** in the upper-right navigation, then **Models** in the left pane, and open your deployment. The **Details** tab shows the **Target URI** (your endpoint URL) and **Key** (your API key). If the **New Foundry** toggle is off, you are in the classic portal layout. There, open **Model catalog** in the left pane to find and deploy a model, and open **Models + endpoints** (under **My assets**) to view your deployments and their endpoint details. The deployment name you choose becomes the value you pass in the `model` parameter of your API requests. You can create multiple deployments of the same model with different names to manage separate configurations or rate limits. ## Authentication Claude in Microsoft Foundry supports two authentication methods: API keys and Entra ID tokens. Both methods use Azure-hosted endpoints in the format `https://{resource}.services.ai.azure.com/anthropic/v1/*`. ### API key authentication After provisioning your Foundry Claude resource, you can obtain an API key from the Foundry portal: 1. In the Foundry portal, select **Build** in the upper-right navigation, then **Models** in the left pane. 2. Open your Claude deployment and select the **Details** tab. 3. Copy the **Key** value (and note the **Target URI** for your endpoint). 4. Use either the `api-key` or `x-api-key` header in your requests, or provide it to the SDK. The Foundry SDKs require an API key and either a resource name or base URL. The C#, Java, PHP, Python, and TypeScript SDKs automatically read these from the following environment variables if they are defined: * `ANTHROPIC_FOUNDRY_API_KEY` - Your API key * `ANTHROPIC_FOUNDRY_RESOURCE` - Your resource name (for example, `example-resource`) * `ANTHROPIC_FOUNDRY_BASE_URL` - Alternative to resource name: the full base URL (for example, `https://example-resource.services.ai.azure.com/anthropic/`). The C# SDK does not read this variable: it always constructs the base URL from the resource name. The `resource` and `base_url` parameters are mutually exclusive. Provide either the resource name (which the SDK uses to construct the URL as `https://{resource}.services.ai.azure.com/anthropic/`) or the full base URL directly. **Example using API key:** ```bash cURL curl https://{resource}.services.ai.azure.com/anthropic/v1/messages \ -H "content-type: application/json" \ -H "api-key: YOUR_AZURE_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "messages": [ {"role": "user", "content": "Hello!"} ] }' ``` ```bash CLI # ant reads ANTHROPIC_API_KEY and sends it as x-api-key, which Foundry accepts export ANTHROPIC_API_KEY="YOUR_AZURE_API_KEY" ant messages create \ --base-url https://example-resource.services.ai.azure.com/anthropic \ --model claude-opus-5 \ --max-tokens 1024 \ --message '{role: user, content: "Hello!"}' \ --transform content ``` ```python Python import os from anthropic import AnthropicFoundry client = AnthropicFoundry( api_key=os.environ.get("ANTHROPIC_FOUNDRY_API_KEY"), resource="example-resource", # your resource name ) message = client.messages.create( model="claude-opus-5", max_tokens=1024, messages=[{"role": "user", "content": "Hello!"}], ) print(message.content) ``` ```typescript TypeScript import AnthropicFoundry from "@anthropic-ai/foundry-sdk"; const client = new AnthropicFoundry({ apiKey: process.env.ANTHROPIC_FOUNDRY_API_KEY, resource: "example-resource" // your resource name }); const message = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, messages: [{ role: "user", content: "Hello!" }] }); console.log(message.content); ``` ```csharp C# using Anthropic.Foundry; using Anthropic.Models.Messages; var client = new AnthropicFoundryClient( new AnthropicFoundryApiKeyCredentials( Environment.GetEnvironmentVariable("ANTHROPIC_FOUNDRY_API_KEY")!, "example-resource" ) ); var response = await client.Messages.Create(new MessageCreateParams { Model = "claude-opus-5", MaxTokens = 1024, Messages = [new() { Role = Role.User, Content = "Hello!" }], }); Console.WriteLine( string.Join("", response.Content .Select(block => block.Value) .OfType() .Select(textBlock => textBlock.Text))); ``` ```go Go // The Go SDK does not yet support Foundry natively. This example uses the // standard Go SDK as a workaround. WithoutEnvironmentDefaults keeps the // client from also reading ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN from // the environment and sending a Claude API credential to your Foundry // endpoint. Features that Foundry does not support fail server-side rather // than client-side. For full Foundry support, use the C#, Java, PHP, // Python, or TypeScript SDKs. package main import ( "context" "fmt" "os" "github.com/anthropics/anthropic-sdk-go" "github.com/anthropics/anthropic-sdk-go/option" ) func main() { client := anthropic.NewClient( option.WithoutEnvironmentDefaults(), option.WithBaseURL("https://example-resource.services.ai.azure.com/anthropic"), option.WithAPIKey(os.Getenv("ANTHROPIC_FOUNDRY_API_KEY")), ) message, err := client.Messages.New(context.Background(), anthropic.MessageNewParams{ Model: "claude-opus-5", MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Hello!")), }, }) if err != nil { panic(err) } fmt.Println(message.Content) } ``` ```java Java import com.anthropic.client.AnthropicClient; import com.anthropic.client.okhttp.AnthropicOkHttpClient; import com.anthropic.foundry.backends.FoundryBackend; import com.anthropic.models.messages.MessageCreateParams; void main() { // Requires env vars: ANTHROPIC_FOUNDRY_API_KEY, ANTHROPIC_FOUNDRY_RESOURCE AnthropicClient client = AnthropicOkHttpClient.builder() .backend(FoundryBackend.fromEnv()) .build(); MessageCreateParams params = MessageCreateParams.builder() .model("claude-opus-5") .maxTokens(1024) .addUserMessage("Hello!") .build(); client.messages().create(params).content().stream() .flatMap(block -> block.text().stream()) .forEach(textBlock -> IO.println(textBlock.text())); } ``` ```php PHP use Anthropic\Foundry; $client = Foundry\Client::withCredentials( apiKey: getenv('ANTHROPIC_FOUNDRY_API_KEY'), baseUrl: 'https://example-resource.services.ai.azure.com/anthropic', ); $message = $client->messages->create( maxTokens: 1024, messages: [ ['role' => 'user', 'content' => 'Hello!'] ], model: 'claude-opus-5', ); echo array_find($message->content, fn ($block) => $block->type === 'text')->text; ``` ```ruby Ruby # The Ruby SDK does not yet support Foundry natively. This example uses the # standard Ruby SDK as a workaround. Pass credentials explicitly: without # them, the client falls back to the ANTHROPIC_API_KEY or # ANTHROPIC_AUTH_TOKEN environment variables and could send a Claude API # credential to your Foundry endpoint. Features that Foundry # does not support fail server-side rather than client-side. For full # Foundry support, use the C#, Java, PHP, Python, or TypeScript SDKs. require "anthropic" client = Anthropic::Client.new( base_url: "https://example-resource.services.ai.azure.com/anthropic", api_key: ENV.fetch("ANTHROPIC_FOUNDRY_API_KEY") ) message = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [{role: "user", content: "Hello!"}] ) puts message.content.find { it.type == :text }.text ``` Keep your API keys secure. Never commit them to version control or share them publicly. Anyone with access to your API key can make requests to Claude through your Foundry resource. ### Microsoft Entra authentication Entra ID authentication lets you manage access with Azure RBAC, integrate with your organization's identity management, and avoid handling API keys manually. To use Entra ID tokens: 1. Enable [Microsoft Entra ID authentication](https://learn.microsoft.com/en-us/azure/ai-foundry/model-inference/how-to/configure-entra-id) for your Foundry resource. 2. Obtain an access token from Entra ID. 3. Use the token in the `Authorization: Bearer {TOKEN}` header. **Example using Entra ID:** ```bash cURL # Get Microsoft Entra ID token ACCESS_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv) # Make request with token. Replace {resource} with your resource name curl https://{resource}.services.ai.azure.com/anthropic/v1/messages \ -H "content-type: application/json" \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "messages": [ {"role": "user", "content": "Hello!"} ] }' ``` ```bash CLI # The ant CLI can send a bearer token with --auth-token, but a set # ANTHROPIC_API_KEY environment variable takes precedence over it (the CLI # prints only a console notice), so your request could authenticate with # the wrong credential. For the Entra ID flow, use the cURL example or one # of the SDK examples instead. ``` ```python Python from anthropic import AnthropicFoundry from azure.identity import DefaultAzureCredential, get_bearer_token_provider # Get Microsoft Entra ID token using token provider pattern token_provider = get_bearer_token_provider( DefaultAzureCredential(), "https://ai.azure.com/.default" ) # Create client with Entra ID authentication client = AnthropicFoundry( resource="example-resource", # your resource name azure_ad_token_provider=token_provider, # Use token provider for Entra ID auth ) # Make request message = client.messages.create( model="claude-opus-5", max_tokens=1024, messages=[{"role": "user", "content": "Hello!"}], ) print(message.content) ``` ```typescript TypeScript import AnthropicFoundry from "@anthropic-ai/foundry-sdk"; import { DefaultAzureCredential, getBearerTokenProvider } from "@azure/identity"; // Get Entra ID token using token provider pattern const credential = new DefaultAzureCredential(); const tokenProvider = getBearerTokenProvider(credential, "https://ai.azure.com/.default"); // Create client with Entra ID authentication const client = new AnthropicFoundry({ resource: "example-resource", // your resource name azureADTokenProvider: tokenProvider // Use token provider for Entra ID auth }); // Make request const message = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, messages: [{ role: "user", content: "Hello!" }] }); console.log(message.content); ``` ```csharp C# using Anthropic.Foundry; using Anthropic.Models.Messages; using Azure.Identity; var client = new AnthropicFoundryClient( new AnthropicFoundryIdentityTokenCredentials( new DefaultAzureCredential(), "example-resource" ) ); var response = await client.Messages.Create(new MessageCreateParams { Model = "claude-opus-5", MaxTokens = 1024, Messages = [new() { Role = Role.User, Content = "Hello!" }], }); Console.WriteLine( string.Join("", response.Content .Select(block => block.Value) .OfType() .Select(textBlock => textBlock.Text))); ``` ```go Go // The Go SDK does not yet support Foundry natively. This example uses the // standard Go SDK as a workaround, with a static Entra ID token: automatic // token refresh is not built in, so your application must refresh tokens // itself (they typically expire after 1 hour). WithoutEnvironmentDefaults // keeps the client from also reading ANTHROPIC_API_KEY or // ANTHROPIC_AUTH_TOKEN from the environment and sending a Claude API // credential to your Foundry endpoint. For full Foundry support, use the // C#, Java, PHP, Python, or TypeScript SDKs. package main import ( "context" "fmt" "os" "github.com/anthropics/anthropic-sdk-go" "github.com/anthropics/anthropic-sdk-go/option" ) func main() { // Obtain an Entra ID access token, for example using the Azure CLI: // az account get-access-token --resource https://ai.azure.com \ // --query accessToken -o tsv client := anthropic.NewClient( option.WithoutEnvironmentDefaults(), option.WithBaseURL("https://example-resource.services.ai.azure.com/anthropic"), option.WithAuthToken(os.Getenv("AZURE_ACCESS_TOKEN")), ) message, err := client.Messages.New(context.Background(), anthropic.MessageNewParams{ Model: "claude-opus-5", MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Hello!")), }, }) if err != nil { panic(err) } fmt.Println(message.Content) } ``` ```java Java import com.anthropic.client.AnthropicClient; import com.anthropic.client.okhttp.AnthropicOkHttpClient; import com.anthropic.foundry.backends.FoundryBackend; import com.anthropic.models.messages.MessageCreateParams; import com.azure.identity.AuthenticationUtil; import com.azure.identity.DefaultAzureCredentialBuilder; import java.util.function.Supplier; void main() { Supplier bearerTokenSupplier = AuthenticationUtil.getBearerTokenSupplier( new DefaultAzureCredentialBuilder().build(), "https://ai.azure.com/.default" ); AnthropicClient client = AnthropicOkHttpClient.builder() .backend(FoundryBackend.builder() .bearerTokenSupplier(bearerTokenSupplier) .resource("example-resource") .build()) .build(); MessageCreateParams params = MessageCreateParams.builder() .model("claude-opus-5") .maxTokens(1024) .addUserMessage("Hello!") .build(); client.messages().create(params).content().stream() .flatMap(block -> block.text().stream()) .forEach(textBlock -> IO.println(textBlock.text())); } ``` ```php PHP use Anthropic\Foundry; // Obtain an Entra ID access token, for example using the Azure CLI: // az account get-access-token --resource https://ai.azure.com \ // --query accessToken -o tsv $token = getenv('AZURE_ACCESS_TOKEN'); $client = Foundry\Client::withCredentials( authToken: $token, baseUrl: 'https://example-resource.services.ai.azure.com/anthropic', ); $message = $client->messages->create( maxTokens: 1024, messages: [ ['role' => 'user', 'content' => 'Hello!'] ], model: 'claude-opus-5', ); echo array_find($message->content, fn ($block) => $block->type === 'text')->text; ``` ```ruby Ruby # The Ruby SDK does not yet support Foundry natively. This example uses the # standard Ruby SDK as a workaround, with a static Entra ID token: automatic # token refresh is not built in, so your application must refresh tokens # itself (they typically expire after 1 hour). Pass credentials explicitly: # without them, the client falls back to the ANTHROPIC_API_KEY or # ANTHROPIC_AUTH_TOKEN environment variables. For full Foundry support, use # the C#, Java, PHP, Python, or TypeScript SDKs. require "anthropic" # Obtain an Entra ID access token, for example using the Azure CLI: # az account get-access-token --resource https://ai.azure.com \ # --query accessToken -o tsv client = Anthropic::Client.new( base_url: "https://example-resource.services.ai.azure.com/anthropic", auth_token: ENV.fetch("AZURE_ACCESS_TOKEN") ) message = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [{role: "user", content: "Hello!"}] ) puts message.content.find { it.type == :text }.text ``` ## Correlation request IDs Foundry includes request identifiers in HTTP response headers for debugging and tracing. When contacting support, provide both the `request-id` and `apim-request-id` (Azure API Management) values to help teams quickly locate and investigate your request across both Anthropic and Azure systems. ## Feature support Claude in Microsoft Foundry supports most Claude features. You can find all the features currently supported in [Features overview](https://platform.claude.com/docs/en/build-with-claude/overview). ### Context window Claude Fable 5, Claude Opus 5, Claude Opus 4.8, Claude Opus 4.7, Claude Opus 4.6, Claude Sonnet 5, and Claude Sonnet 4.6 have a [1M-token context window](https://platform.claude.com/docs/en/build-with-claude/context-windows) on Microsoft Foundry. Other Claude models, including Claude Sonnet 4.5, have a 200k-token context window. ### Claude features not supported for Claude in Microsoft Foundry * Admin API * Advisor tool * Claude Managed Agents * Compliance API * Models API * Message Batches API * Server-side fallback (the [`fallbacks` parameter](https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback#server-side-fallback); use the [client-side fallback pattern](https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback#client-side-fallback) instead) ### Additional features not supported when hosted on Azure The following features are available for deployments hosted on Anthropic but are not supported for deployments hosted on Azure: * Structured outputs * Server-side tools (web search, web fetch, code execution, and tool search) * MCP connector * Agent Skills * Programmatic tool calling * Files API Requests that use these features against a deployment hosted on Azure return a `400 Bad Request` error by design. Claude Code detects deployments hosted on Azure and automatically adapts its feature set. ## API responses API responses from Claude in Microsoft Foundry follow the standard [Claude API response format](https://platform.claude.com/docs/en/api/messages/create). This includes the `usage` object in response bodies, which provides detailed token consumption information for your requests. The `usage` object is consistent across all platforms (Claude API, Amazon Bedrock, Claude Platform on AWS, Foundry, and Google Cloud). For details on response headers specific to Foundry, see [Correlation request IDs](https://platform.claude.com/docs/en/build-with-claude/claude-in-microsoft-foundry#correlation-request-ids). ## API model IDs and deployments Lifecycle terms (Deprecated, Retired) are defined in [Model deprecations](https://platform.claude.com/docs/en/about-claude/model-deprecations). Microsoft Foundry follows the Claude API lifecycle schedule. The following Claude models are available through Foundry: | Model | Default deployment name | Hosted on Azure | Hosted on Anthropic | | ----------------- | ----------------------- | --------------- | ------------------- | | Claude Fable 5 | claude-fable-5 | | ✓ | | Claude Opus 5 | claude-opus-5 | ✓ | ✓ | | Claude Opus 4.8 | claude-opus-4-8 | ✓ | ✓ | | Claude Opus 4.7 | claude-opus-4-7 | | ✓ | | Claude Opus 4.6 | claude-opus-4-6 | | ✓ | | Claude Opus 4.5 | claude-opus-4-5 | | ✓ | | Claude Sonnet 5 | claude-sonnet-5 | ✓ | ✓ | | Claude Sonnet 4.6 | claude-sonnet-4-6 | | ✓ | | Claude Sonnet 4.5 | claude-sonnet-4-5 | | ✓ | | Claude Haiku 4.5 | claude-haiku-4-5 | ✓ | ✓ | By default, deployment names match the model IDs shown in the preceding table. However, you can create custom deployments with different names in the Foundry portal to manage different configurations, versions, or rate limits. Use the deployment name (not necessarily the model ID) in your API requests. [Claude Mythos Preview](https://anthropic.com/glasswing) is a research preview available to invited customers on Microsoft Foundry. Upgrading to a newer Claude model? In Claude Code, run `/claude-api migrate` to apply model ID swaps and breaking parameter changes across your codebase. The skill detects which cloud platform your code targets and adjusts model ID formats and feature changes for that platform. See [Migrating to a newer Claude model](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/claude-api-skill#migrating-to-a-newer-claude-model). ## Billing Claude in Microsoft Foundry bills through the [Azure Marketplace](https://azuremarketplace.microsoft.com/). Usage is denominated in Claude Consumption Units (CCUs), metered hourly, and invoiced monthly in arrears on your Azure bill. CCUs are not prepaid credits. There is no CCU balance or commitment. For the CCU price, conversion mechanics, and per-model token rates, see [Claude in Microsoft Foundry pricing](https://platform.claude.com/docs/en/about-claude/pricing#claude-in-microsoft-foundry-pricing). ## Migrating between hosting options To move an existing deployment from one hosting option to the other: 1. Create a new deployment of the model's other hosting version (Hosted on Azure or Hosted on Anthropic). This can be in the same Foundry resource, or a new one. 2. Update your application to pass the new deployment name in the `model` parameter. 3. Delete the old deployment once traffic has moved. If the new deployment is in the same Foundry resource, your endpoint URL and authentication are unchanged. If you created a new resource, update your application's endpoint and credentials to point to it. ## Monitoring and logging Azure provides monitoring and logging for your Claude usage through standard Azure patterns: * **Azure Monitor:** Track API usage, latency, and error rates * **Azure Log Analytics:** Query and analyze request/response logs * **Cost Management:** Monitor and forecast costs associated with Claude usage Anthropic recommends logging your activity on at least a 30-day rolling basis to understand usage patterns and investigate any potential issues. Azure's logging services are configured within your Azure subscription. Enabling logging does not provide Microsoft or Anthropic access to your content beyond what's necessary for billing and service operation. ## Troubleshooting ### Authentication errors **Error:** `401 Unauthorized` or `Invalid API key` * **Solution:** Verify your API key is correct. You can find it in the Foundry portal on your deployment's **Details** tab (under **Build** > **Models**). * **Solution:** If using Microsoft Entra ID, ensure your access token is valid and hasn't expired. Tokens typically expire after 1 hour. **Error:** `403 Forbidden` * **Solution:** Your Azure account may lack the necessary permissions. Ensure you have the appropriate Azure RBAC role assigned (for example, **Foundry User** (formerly Azure AI User) or **Cognitive Services User**). ### Rate limiting **Error:** `429 Too Many Requests` * **Solution:** You've exceeded your rate limit. Implement exponential backoff and retry logic in your application. * **Solution:** Consider requesting rate limit increases through the Azure portal or Azure support. #### Rate limit headers Foundry does not include Anthropic's standard rate limit headers (`anthropic-ratelimit-tokens-limit`, `anthropic-ratelimit-tokens-remaining`, `anthropic-ratelimit-tokens-reset`, `anthropic-ratelimit-input-tokens-limit`, `anthropic-ratelimit-input-tokens-remaining`, `anthropic-ratelimit-input-tokens-reset`, `anthropic-ratelimit-output-tokens-limit`, `anthropic-ratelimit-output-tokens-remaining`, and `anthropic-ratelimit-output-tokens-reset`) in responses. Manage rate limiting through Azure's monitoring tools instead. ### Model and deployment errors **Error:** `Model not found` or `Deployment not found` * **Solution:** Verify you're using the correct deployment name. If you haven't created a custom deployment, use the default model ID (for example, claude-opus-5). * **Solution:** Ensure the model/deployment is available in your Azure region. **Error:** `Invalid model parameter` * **Solution:** The model parameter should contain your deployment name, which can be customized in the Foundry portal. Verify the deployment exists and is properly configured. ## Next steps Explore Claude's advanced features and capabilities. Learn about Anthropic's pricing structure for models and features. As safer and more capable models launch, Anthropic regularly retires older ones. See all API deprecations, along with recommended replacements. ## Additional resources Browse Anthropic models in the Foundry catalog. View Microsoft's pricing details for Azure AI Foundry. View Anthropic's per-model pricing details. Manage your Azure resources. --- title: Claude on Amazon Bedrock (Opus 4.6 and earlier) url: https://platform.claude.com/docs/en/build-with-claude/claude-on-amazon-bedrock-legacy description: The legacy Amazon Bedrock integration for Claude models, using InvokeModel and Converse APIs with ARN-versioned model identifiers. --- This page covers the legacy Amazon Bedrock integration: the `InvokeModel` and `Converse` APIs with ARN-versioned model identifiers and AWS event-stream encoding. For models available on the Messages-API Bedrock endpoint, see [Claude in Amazon Bedrock](https://platform.claude.com/docs/en/build-with-claude/claude-in-amazon-bedrock), which uses the Messages API at `/anthropic/v1/messages` with SSE streaming. For an Anthropic-operated alternative with AWS Marketplace billing and typically same-day feature access, see [Claude Platform on AWS](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws). Existing Bedrock users can follow the [migration guide](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#migrating-from-amazon-bedrock). Calling Claude through Bedrock slightly differs from how you would call Claude on the Claude API directly. This guide walks you through completing an API call to Claude on Bedrock using one of Anthropic's [client SDKs](https://platform.claude.com/docs/en/cli-sdks-libraries/overview). Note that this guide assumes you have already signed up for an [AWS account](https://portal.aws.amazon.com/billing/signup) and configured programmatic access. ## Install and configure the AWS CLI 1. [Install a version of the AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-welcome.html) at or newer than version `2.13.23`. 2. Configure your AWS credentials using the AWS configure command (see [Configure the AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html)) or find your credentials by navigating to "Command line or programmatic access" within your AWS dashboard and following the directions in the modal window. 3. Verify that your credentials are working: ```bash AWS CLI aws sts get-caller-identity ``` ## Install an SDK for accessing Bedrock Anthropic's [client SDKs](https://platform.claude.com/docs/en/cli-sdks-libraries/overview) support Bedrock. You can also use an AWS SDK like `boto3` directly. ```bash pip install -U "anthropic[bedrock]" ``` ```bash npm install @anthropic-ai/bedrock-sdk ``` ```bash dotnet add package Anthropic.Bedrock ``` ```bash go get github.com/anthropics/anthropic-sdk-go/bedrock ``` ```groovy Gradle implementation("com.anthropic:anthropic-java-bedrock:2.53.0") ``` ```xml Maven com.anthropic anthropic-java-bedrock 2.53.0 ``` ```java Java import com.anthropic.client.AnthropicClient; import com.anthropic.client.okhttp.AnthropicOkHttpClient; import com.anthropic.bedrock.backends.BedrockBackend; import com.anthropic.models.messages.MessageCreateParams; import com.anthropic.models.messages.Message; import com.anthropic.models.messages.Model; public class BasicMessage { public static void main(String[] args) { AnthropicClient client = AnthropicOkHttpClient.builder() .backend(BedrockBackend.fromEnv()) .build(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_4_6) .maxTokens(1024L) .addUserMessage("What is the capital of France?") .build(); Message response = client.messages().create(params); response.content().stream() .flatMap(block -> block.text().stream()) .forEach(textBlock -> System.out.println(textBlock.text())); } } ``` ```bash composer require anthropic-ai/sdk aws/aws-sdk-php ``` ```bash # Gemfile gem "anthropic" gem "aws-sdk-bedrockruntime" ``` ```bash pip install "boto3>=1.28.59" ``` ## Accessing Bedrock ### Subscribe to Anthropic models Go to the [AWS Console > Bedrock > Model Access](https://console.aws.amazon.com/bedrock/home?region=us-west-2#/modelaccess) and request access to Anthropic models. Note that Anthropic model availability varies by region. See [AWS documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/models-regions.html) for latest information. #### API model IDs Claude Opus 5, Claude Sonnet 5, Claude Fable 5, Claude Opus 4.8, and Claude Opus 4.7 are reachable through `InvokeModel` on `bedrock-runtime`. These requests are served by the same infrastructure as the [Claude in Amazon Bedrock](https://platform.claude.com/docs/en/build-with-claude/claude-in-amazon-bedrock) endpoint. For the native Messages API request shape and full feature parity, use that page. These models are omitted from the model table on this page because they do not have ARN-versioned model IDs. Lifecycle terms (Deprecated, Retired) are defined in [Model deprecations](https://platform.claude.com/docs/en/about-claude/model-deprecations). Lifecycle dates on partner-operated platforms are set by the partner and can differ from the Claude API schedule. For the current retirement date of any model on Amazon Bedrock, see [Amazon Bedrock's model lifecycle page](https://docs.aws.amazon.com/bedrock/latest/userguide/model-lifecycle.html). AWS offers newer Claude models through [cross-region inference](https://docs.aws.amazon.com/bedrock/latest/userguide/cross-region-inference.html) rather than on-demand throughput. For these models, a request that passes the base model ID fails with an HTTP 400 error like the following: ```text wrap Invocation of model ID anthropic.claude-sonnet-4-5-20250929-v1:0 with on-demand throughput isn't supported. Retry your request with the ID or ARN of an inference profile that contains this model. ``` To invoke these models, pass an inference profile instead of the base model ID. The inference profile ID is the base model ID with a prefix from a column marked "Yes" in the following table, for example us.anthropic.claude-sonnet-4-5-20250929-v1:0. You can also pass the full inference profile ARN, in the form `arn:aws:bedrock:{region}:{account-id}:inference-profile/{inference-profile-id}`. For AWS's authoritative list of available inference profiles, see [Supported Regions and models for inference profiles](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html). For how the prefixes affect routing and pricing, see the [Global versus regional endpoints](https://platform.claude.com/docs/en/build-with-claude/claude-on-amazon-bedrock-legacy#global-vs-regional-endpoints) section. | Model | Base Bedrock model ID | `global` | `us` | `eu` | `jp` | `apac` | | ---------------------------- | ----------------------------------------- | -------- | ---- | ---- | ---- | ------ | | Claude Opus 4.6 | anthropic.claude-opus-4-6-v1 | Yes | Yes | Yes | Yes | Yes | | Claude Sonnet 4.6 | anthropic.claude-sonnet-4-6 | Yes | Yes | Yes | Yes | No | | Claude Sonnet 4.5 | anthropic.claude-sonnet-4-5-20250929-v1:0 | Yes | Yes | Yes | Yes | No | | Claude Sonnet 4 Deprecated. | anthropic.claude-sonnet-4-20250514-v1:0 | Yes | Yes | Yes | No | Yes | | Claude Sonnet 3.7 Retired. | anthropic.claude-3-7-sonnet-20250219-v1:0 | No | No | No | No | No | | Claude Opus 4.5 | anthropic.claude-opus-4-5-20251101-v1:0 | Yes | Yes | Yes | No | No | | Claude Opus 4.1 Deprecated. | anthropic.claude-opus-4-1-20250805-v1:0 | No | Yes | No | No | No | | Claude Opus 4 Retired. | anthropic.claude-opus-4-20250514-v1:0 | No | No | No | No | No | | Claude Haiku 4.5 | anthropic.claude-haiku-4-5-20251001-v1:0 | Yes | Yes | Yes | No | No | | Claude Haiku 3.5 Deprecated. | anthropic.claude-3-5-haiku-20241022-v1:0 | No | Yes | No | No | No | ### List available models The following examples show how to print a list of all the Claude models available through Bedrock: ```bash AWS CLI aws bedrock list-foundation-models --region=us-west-2 --by-provider anthropic --query "modelSummaries[*].modelId" ``` ```python Boto3 (Python) import boto3 bedrock = boto3.client(service_name="bedrock") response = bedrock.list_foundation_models(byProvider="anthropic") for summary in response["modelSummaries"]: print(summary["modelId"]) ``` ```typescript TypeScript import { BedrockClient, ListFoundationModelsCommand } from "@aws-sdk/client-bedrock"; const client = new BedrockClient({ region: "us-west-2" }); const command = new ListFoundationModelsCommand({ byProvider: "anthropic" }); const response = await client.send(command); if (response.modelSummaries) { for (const summary of response.modelSummaries) { console.log(summary.modelId); } } ``` ```csharp C# using Amazon; using Amazon.Bedrock; using Amazon.Bedrock.Model; var client = new AmazonBedrockClient(RegionEndpoint.USWest2); var request = new ListFoundationModelsRequest { ByProvider = "anthropic" }; var response = await client.ListFoundationModelsAsync(request); foreach (var summary in response.ModelSummaries) { Console.WriteLine(summary.ModelId); } ``` ```go Go import ( "context" "fmt" "log" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/bedrock" ) // ... cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion("us-west-2")) if err != nil { log.Fatal(err) } client := bedrock.NewFromConfig(cfg) byProvider := "anthropic" response, err := client.ListFoundationModels(context.TODO(), &bedrock.ListFoundationModelsInput{ ByProvider: &byProvider, }) if err != nil { log.Fatal(err) } for _, summary := range response.ModelSummaries { fmt.Println(*summary.ModelId) } ``` ```java Java import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.bedrock.BedrockClient; import software.amazon.awssdk.services.bedrock.model.ListFoundationModelsRequest; import software.amazon.awssdk.services.bedrock.model.ListFoundationModelsResponse; import software.amazon.awssdk.services.bedrock.model.FoundationModelSummary; public class ListAnthropicModels { public static void main(String[] args) { BedrockClient client = BedrockClient.builder() .region(Region.US_WEST_2) .build(); ListFoundationModelsRequest request = ListFoundationModelsRequest.builder() .byProvider("anthropic") .build(); ListFoundationModelsResponse response = client.listFoundationModels(request); for (FoundationModelSummary summary : response.modelSummaries()) { System.out.println(summary.modelId()); } client.close(); } } ``` ```php PHP 'us-west-2', 'version' => 'latest' ]); $result = $client->listFoundationModels([ 'byProvider' => 'anthropic' ]); foreach ($result['modelSummaries'] as $summary) { echo $summary['modelId'] . PHP_EOL; } ``` ```ruby Ruby require "aws-sdk-bedrock" client = Aws::Bedrock::Client.new(region: "us-west-2") response = client.list_foundation_models({ by_provider: "anthropic" }) response.model_summaries.each do |summary| puts summary.model_id end ``` ### Making requests The following examples show how to generate text from Claude on Bedrock: Calling the `InvokeModel` API with AWS credentials requires SigV4 request signing, which the SDKs in the other tabs handle automatically. For a Bedrock endpoint you can call with a self-contained cURL command, see [Claude in Amazon Bedrock](https://platform.claude.com/docs/en/build-with-claude/claude-in-amazon-bedrock#making-your-first-request). The `ant` CLI does not support Amazon Bedrock. Use one of the SDK examples instead. ```python from anthropic import AnthropicBedrock client = AnthropicBedrock( # Authenticate by either providing the keys below or use the default AWS credential providers, such as # using ~/.aws/credentials or the "AWS_SECRET_ACCESS_KEY" and "AWS_ACCESS_KEY_ID" environment variables. aws_access_key="", aws_secret_key="", # Temporary credentials can be used with aws_session_token. # Read more at https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp.html. aws_session_token="", # aws_region changes the aws region to which the request is made. By default, the SDK reads AWS_REGION, # and if that's not present, defaults to us-east-1. Note that the SDK does not read ~/.aws/config for the region. aws_region="us-west-2", ) message = client.messages.create( model="global.anthropic.claude-opus-4-6-v1", max_tokens=256, messages=[{"role": "user", "content": "Hello, world"}], ) print(message.content) ``` ```typescript import AnthropicBedrock from "@anthropic-ai/bedrock-sdk"; const client = new AnthropicBedrock({ // Authenticate by either providing the keys below or use // the default AWS credential providers, such as // ~/.aws/credentials or the "AWS_SECRET_ACCESS_KEY" and // "AWS_ACCESS_KEY_ID" environment variables. awsAccessKey: "", awsSecretKey: "", // Temporary credentials can be used with awsSessionToken. // Read more at https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp.html. awsSessionToken: "", // awsRegion changes the aws region to which the request // is made. By default, the SDK reads AWS_REGION, and if // that's not present, defaults to us-east-1. Note that // the SDK does not read ~/.aws/config for the region. awsRegion: "us-west-2" }); const message = await client.messages.create({ model: "global.anthropic.claude-opus-4-6-v1", max_tokens: 256, messages: [{ role: "user", content: "Hello, world" }] }); console.log(message); ``` ```csharp using Anthropic.Bedrock; using Anthropic.Models.Messages; AnthropicBedrockClient client = new( await AnthropicBedrockCredentialsHelper.FromEnv() ?? throw new InvalidOperationException("AWS credentials not configured.") ); var response = await client.Messages.Create(new MessageCreateParams { Model = "global.anthropic.claude-opus-4-6-v1", MaxTokens = 256, Messages = [new() { Role = Role.User, Content = "Hello, world" }], }); Console.WriteLine( string.Join("", response.Content .Select(block => block.Value) .OfType() .Select(textBlock => textBlock.Text))); ``` ```go import ( "context" "fmt" "github.com/anthropics/anthropic-sdk-go" "github.com/anthropics/anthropic-sdk-go/bedrock" ) // ... // Uses default AWS credential provider chain client := anthropic.NewClient( bedrock.WithLoadDefaultConfig(context.Background()), ) message, err := client.Messages.New(context.Background(), anthropic.MessageNewParams{ Model: "global.anthropic.claude-opus-4-6-v1", MaxTokens: 256, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Hello, world")), }, }) if err != nil { panic(err) } fmt.Println(message.Content) ``` ```java import com.anthropic.bedrock.backends.BedrockBackend; import com.anthropic.client.AnthropicClient; import com.anthropic.client.okhttp.AnthropicOkHttpClient; import com.anthropic.models.messages.Message; import com.anthropic.models.messages.MessageCreateParams; public class BedrockExample { public static void main(String[] args) { // Uses default AWS credential provider chain AnthropicClient client = AnthropicOkHttpClient.builder() .backend(BedrockBackend.fromEnv()) .build(); Message message = client .messages() .create( MessageCreateParams.builder() .model("global.anthropic.claude-opus-4-6-v1") .maxTokens(256) .addUserMessage("Hello, world") .build() ); System.out.println(message.content()); } } ``` ```php messages->create( maxTokens: 256, messages: [ ['role' => 'user', 'content' => 'Hello, world'] ], model: 'global.anthropic.claude-opus-4-6-v1', ); echo $message->content[0]->text; ``` ```ruby require "anthropic" client = Anthropic::BedrockClient.new message = client.messages.create( model: "global.anthropic.claude-opus-4-6-v1", max_tokens: 256, messages: [{role: "user", content: "Hello, world"}] ) puts message.content.first.text ``` ```python import boto3 import json bedrock = boto3.client(service_name="bedrock-runtime") body = json.dumps( { "max_tokens": 256, "messages": [{"role": "user", "content": "Hello, world"}], "anthropic_version": "bedrock-2023-05-31", } ) response = bedrock.invoke_model( body=body, modelId="global.anthropic.claude-opus-4-6-v1" ) response_body = json.loads(response.get("body").read()) print(response_body.get("content")) ``` See the [client SDKs](https://platform.claude.com/docs/en/cli-sdks-libraries/overview) for more details, and the [official Bedrock documentation](https://docs.aws.amazon.com/bedrock/). ### Bearer token authentication You can authenticate with Bedrock using bearer tokens instead of AWS credentials. This is useful in corporate environments where teams need access to Bedrock without managing AWS credentials, IAM roles, or account-level permissions. The simplest approach is to set the `AWS_BEARER_TOKEN_BEDROCK` environment variable, which each SDK detects automatically when resolving credentials from the environment. To provide a token programmatically: This section shows how to configure a bearer token in an SDK client. The SDKs also read the token from the `AWS_BEARER_TOKEN_BEDROCK` environment variable. For making direct HTTP requests with a bearer token, see the [Amazon Bedrock documentation](https://docs.aws.amazon.com/bedrock/). The `ant` CLI does not support Amazon Bedrock. Use one of the SDK examples instead. ```python from anthropic import AnthropicBedrock client = AnthropicBedrock( api_key="your-bearer-token", aws_region="us-west-2", ) message = client.messages.create( model="us.anthropic.claude-sonnet-4-5-20250929-v1:0", max_tokens=1024, messages=[{"role": "user", "content": "Hello!"}], ) print(message.content) ``` ```typescript import AnthropicBedrock from "@anthropic-ai/bedrock-sdk"; const client = new AnthropicBedrock({ apiKey: "your-bearer-token", awsRegion: "us-west-2" }); const message = await client.messages.create({ model: "us.anthropic.claude-sonnet-4-5-20250929-v1:0", max_tokens: 1024, messages: [{ role: "user", content: "Hello!" }] }); console.log(message); ``` ```csharp using Anthropic.Bedrock; using Anthropic.Models.Messages; var client = new AnthropicBedrockClient( new AnthropicBedrockApiTokenCredentials { BearerToken = "your-bearer-token", Region = "us-west-2", } ); var response = await client.Messages.Create(new MessageCreateParams { Model = "us.anthropic.claude-sonnet-4-5-20250929-v1:0", MaxTokens = 1024, Messages = [new() { Role = Role.User, Content = "Hello!" }], }); ``` ```go import ( "context" "fmt" "github.com/anthropics/anthropic-sdk-go" "github.com/anthropics/anthropic-sdk-go/bedrock" "github.com/aws/aws-sdk-go-v2/aws" ) // ... cfg := aws.Config{ Region: "us-west-2", BearerAuthTokenProvider: bedrock.NewStaticBearerTokenProvider("your-bearer-token"), } client := anthropic.NewClient( bedrock.WithConfig(cfg), ) message, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: "us.anthropic.claude-sonnet-4-5-20250929-v1:0", MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Hello!")), }, }) if err != nil { panic(err) } fmt.Println(message.Content[0].Text) ``` ```java import com.anthropic.bedrock.backends.BedrockBackend; import com.anthropic.client.AnthropicClient; import com.anthropic.client.okhttp.AnthropicOkHttpClient; import com.anthropic.models.messages.MessageCreateParams; // Option 1: Set AWS_BEARER_TOKEN_BEDROCK environment variable and use fromEnv() AnthropicClient client = AnthropicOkHttpClient.builder() .backend(BedrockBackend.fromEnv()) .build(); // Option 2: Provide the token programmatically client = AnthropicOkHttpClient.builder() .backend(BedrockBackend.builder() .apiKey("your-bearer-token") .build()) .build(); MessageCreateParams params = MessageCreateParams.builder() .model("us.anthropic.claude-sonnet-4-5-20250929-v1:0") .maxTokens(1024) .addUserMessage("Hello!") .build(); client.messages().create(params).content().stream() .flatMap(block -> block.text().stream()) .forEach(textBlock -> System.out.println(textBlock.text())); ``` ```php messages->create( maxTokens: 1024, messages: [ ['role' => 'user', 'content' => 'Hello!'] ], model: 'us.anthropic.claude-sonnet-4-5-20250929-v1:0', ); echo $message->content[0]->text; ``` ```ruby require "anthropic" client = Anthropic::BedrockClient.new( api_key: "your-bearer-token", aws_region: "us-west-2" ) message = client.messages.create( model: "us.anthropic.claude-sonnet-4-5-20250929-v1:0", max_tokens: 1024, messages: [{role: "user", content: "Hello!"}] ) puts message.content.first.text ``` ## Activity logging Bedrock provides an [invocation logging service](https://docs.aws.amazon.com/bedrock/latest/userguide/model-invocation-logging.html) that allows you to log the prompts and completions associated with your usage. Anthropic recommends that you log your activity on at least a 30-day rolling basis to understand your activity and investigate any potential misuse. Turning on this service does not give AWS or Anthropic any access to your content. ## Feature support For the full feature list with Amazon Bedrock availability, see [Features overview](https://platform.claude.com/docs/en/build-with-claude/overview). ### Supported feature highlights * [Messages API](https://platform.claude.com/docs/en/api/messages/create) * [Prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) * [Thinking](https://platform.claude.com/docs/en/build-with-claude/thinking) * [Tool use](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview), including the [Bash tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/bash-tool), [Computer use tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/computer-use-tool), [Memory tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/memory-tool), and [Text editor tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/text-editor-tool) * [Citations](https://platform.claude.com/docs/en/build-with-claude/citations) * [Structured outputs](https://platform.claude.com/docs/en/build-with-claude/structured-outputs) ### Features not supported * Input sources (URL sources for images and documents, Files API) * Server-side tools (code execution, web search, web fetch, advisor) * Agent infrastructure (Agent Skills, MCP connector, programmatic tool calling) * API endpoints (Message Batches, Models, Admin, Compliance, Usage and Cost) * Claude Managed Agents * Server-side fallback (the [`fallbacks` parameter](https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback#server-side-fallback); use the [client-side fallback pattern](https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback#client-side-fallback) instead) * Automatic prompt caching (the [top-level `cache_control` field](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#automatic-caching); use [explicit cache breakpoints](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#explicit-cache-breakpoints) instead) ### PDF support on Bedrock PDF support is available on Bedrock through both the Converse API and InvokeModel API. For detailed information about PDF processing capabilities and limitations, see [Amazon Bedrock PDF support](https://platform.claude.com/docs/en/build-with-claude/pdf-support#amazon-bedrock-pdf-support). **Important considerations for Converse API users:** * Visual PDF analysis (charts, images, layouts) requires citations to be enabled * Without citations, only basic text extraction is available * For full control without forced citations, use the InvokeModel API ### Mid-conversation system messages on Bedrock [Mid-conversation system messages](https://platform.claude.com/docs/en/build-with-claude/mid-conversation-system-messages) are available through the InvokeModel API for Claude Fable 5 and Claude Opus 4.8. As described in the note under [API model IDs](https://platform.claude.com/docs/en/build-with-claude/claude-on-amazon-bedrock-legacy#api-model-ids), these requests are served by the same infrastructure as the [Claude in Amazon Bedrock](https://platform.claude.com/docs/en/build-with-claude/claude-in-amazon-bedrock) endpoint. No beta header is required. This feature is not available on Claude Sonnet 5; use the top-level `system` field instead. It is not available for the ARN-versioned models in the model table on this page. **For Converse API users:** the Converse API accepts system instructions through its top-level [`system` parameter](https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_Converse.html). To add system instructions mid-conversation, use the InvokeModel API. ### Context window Claude Fable 5, Claude Opus 5, Claude Opus 4.8, Claude Opus 4.7, Claude Opus 4.6, Claude Sonnet 5, and Claude Sonnet 4.6 have a [1M-token context window](https://platform.claude.com/docs/en/build-with-claude/context-windows) on Amazon Bedrock. Other Claude models, including Sonnet 4.5 and Sonnet 4 (deprecated), have a 200k-token context window. Bedrock limits request payloads to 20 MB. When sending large documents or many images, you may reach this limit before the token limit. ## Global versus regional endpoints Starting with **Claude Sonnet 4.5 and all future models**, Bedrock offers two endpoint types: * **Global endpoints:** Dynamic routing for maximum availability * **Regional endpoints:** Guaranteed data routing through specific geographic regions Regional endpoints include a 10% pricing premium over global endpoints. This applies to Claude Sonnet 4.5 and future models only. Older models (Claude Sonnet 4 (deprecated) and earlier) maintain their existing pricing structures. ### When to use each option **Global endpoints (recommended):** * Provide maximum availability and uptime * Dynamically route requests to regions with available capacity * No pricing premium * Best for applications where data residency is flexible **Regional endpoints (CRIS):** * Route traffic through specific geographic regions * Required for data residency and compliance requirements * Available for US, EU, Japan, and Asia-Pacific * 10% pricing premium reflects infrastructure costs for dedicated regional capacity ### Implementation **Using global endpoints (default for Opus 4.6, Sonnet 4.6, and Sonnet 4.5):** The model IDs for Claude Opus 4.6, Sonnet 4.6, and Sonnet 4.5 already include the `global.` prefix: Calling the `InvokeModel` API with AWS credentials requires SigV4 request signing, which the SDKs in the other tabs handle automatically. For a Bedrock endpoint you can call with a self-contained cURL command, see [Claude in Amazon Bedrock](https://platform.claude.com/docs/en/build-with-claude/claude-in-amazon-bedrock#making-your-first-request). The `ant` CLI does not support Amazon Bedrock. Use one of the SDK examples instead. ```python from anthropic import AnthropicBedrock client = AnthropicBedrock(aws_region="us-west-2") message = client.messages.create( model="global.anthropic.claude-opus-4-6-v1", max_tokens=256, messages=[{"role": "user", "content": "Hello, world"}], ) ``` ```typescript import AnthropicBedrock from "@anthropic-ai/bedrock-sdk"; const client = new AnthropicBedrock({ awsRegion: "us-west-2" }); const message = await client.messages.create({ model: "global.anthropic.claude-opus-4-6-v1", max_tokens: 256, messages: [{ role: "user", content: "Hello, world" }] }); ``` ```csharp using Anthropic.Bedrock; using Anthropic.Models.Messages; // C# Bedrock client uses model IDs with region prefix for global routing AnthropicBedrockClient client = new( await AnthropicBedrockCredentialsHelper.FromEnv() ?? throw new InvalidOperationException("AWS credentials not configured.") ); var response = await client.Messages.Create(new MessageCreateParams { // Use "global." prefix for global cross-region inference Model = "global.anthropic.claude-opus-4-6-v1", MaxTokens = 256, Messages = [new() { Role = Role.User, Content = "Hello, world" }], }); ``` ```go import ( "context" "github.com/anthropics/anthropic-sdk-go" "github.com/anthropics/anthropic-sdk-go/bedrock" ) // ... // Uses default AWS credential provider chain client := anthropic.NewClient( bedrock.WithLoadDefaultConfig(context.Background()), ) message, _ := client.Messages.New(context.Background(), anthropic.MessageNewParams{ Model: "global.anthropic.claude-opus-4-6-v1", MaxTokens: 256, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Hello, world")), }, }) ``` ```java import com.anthropic.bedrock.backends.BedrockBackend; import com.anthropic.client.AnthropicClient; import com.anthropic.client.okhttp.AnthropicOkHttpClient; import com.anthropic.models.messages.MessageCreateParams; // Uses default AWS credential provider chain AnthropicClient client = AnthropicOkHttpClient.builder() .backend(BedrockBackend.fromEnv()) .build(); var message = client .messages() .create( MessageCreateParams.builder() .model("global.anthropic.claude-opus-4-6-v1") .maxTokens(256) .addUserMessage("Hello, world") .build() ); ``` ```php messages->create( maxTokens: 256, messages: [ ['role' => 'user', 'content' => 'Hello, world'] ], model: 'global.anthropic.claude-opus-4-6-v1', ); ``` ```ruby require "anthropic" # Default credentials resolve region from AWS_REGION env var client = Anthropic::BedrockClient.new message = client.messages.create( # Use "global." prefix for global cross-region inference model: "global.anthropic.claude-opus-4-6-v1", max_tokens: 256, messages: [{role: "user", content: "Hello, world"}] ) ``` **Using regional endpoints (CRIS):** To use regional endpoints, replace the `global.` prefix with a regional prefix such as `us.`: Calling the `InvokeModel` API with AWS credentials requires SigV4 request signing, which the SDKs in the other tabs handle automatically. For a Bedrock endpoint you can call with a self-contained cURL command, see [Claude in Amazon Bedrock](https://platform.claude.com/docs/en/build-with-claude/claude-in-amazon-bedrock#making-your-first-request). The `ant` CLI does not support Amazon Bedrock. Use one of the SDK examples instead. ```python from anthropic import AnthropicBedrock client = AnthropicBedrock(aws_region="us-west-2") # Using US regional endpoint (CRIS) message = client.messages.create( model="us.anthropic.claude-opus-4-6-v1", # Regional prefix max_tokens=256, messages=[{"role": "user", "content": "Hello, world"}], ) ``` ```typescript import AnthropicBedrock from "@anthropic-ai/bedrock-sdk"; const client = new AnthropicBedrock({ awsRegion: "us-west-2" }); // Using US regional endpoint (CRIS) const message = await client.messages.create({ model: "us.anthropic.claude-opus-4-6-v1", // Regional prefix max_tokens: 256, messages: [{ role: "user", content: "Hello, world" }] }); ``` ```csharp using Anthropic.Bedrock; using Anthropic.Models.Messages; AnthropicBedrockClient client = new( new AnthropicBedrockPrivateKeyCredentials { Region = "us-west-2" } ); // Using US regional endpoint (CRIS) var response = await client.Messages.Create(new MessageCreateParams { Model = "us.anthropic.claude-opus-4-6-v1", // Regional prefix MaxTokens = 256, Messages = [new() { Role = Role.User, Content = "Hello, world" }], }); ``` ```go import ( "context" "github.com/anthropics/anthropic-sdk-go" "github.com/anthropics/anthropic-sdk-go/bedrock" ) // ... // Uses default AWS credential provider chain client := anthropic.NewClient( bedrock.WithLoadDefaultConfig(context.Background()), ) // Using US regional endpoint (CRIS) message, _ := client.Messages.New(context.Background(), anthropic.MessageNewParams{ Model: "us.anthropic.claude-opus-4-6-v1", // Regional prefix MaxTokens: 256, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Hello, world")), }, }) ``` ```java import com.anthropic.bedrock.backends.BedrockBackend; import com.anthropic.client.AnthropicClient; import com.anthropic.client.okhttp.AnthropicOkHttpClient; import com.anthropic.models.messages.MessageCreateParams; // Uses default AWS credential provider chain AnthropicClient client = AnthropicOkHttpClient.builder() .backend(BedrockBackend.fromEnv()) .build(); // Using US regional endpoint (CRIS) var message = client .messages() .create( MessageCreateParams.builder() .model("us.anthropic.claude-opus-4-6-v1") // Regional prefix .maxTokens(256) .addUserMessage("Hello, world") .build() ); ``` ```php messages->create( maxTokens: 256, messages: [ ['role' => 'user', 'content' => 'Hello, world'] ], model: 'us.anthropic.claude-opus-4-6-v1', ); ``` ```ruby require "anthropic" # Using US regional endpoint (CRIS) client = Anthropic::BedrockClient.new(aws_region: "us-west-2") message = client.messages.create( model: "us.anthropic.claude-opus-4-6-v1", # Regional prefix max_tokens: 256, messages: [{role: "user", content: "Hello, world"}] ) ``` **Claude Mythos Preview** is a research preview model available to invited customers on Amazon Bedrock. For more information, see [Project Glasswing](https://anthropic.com/glasswing). ## Additional resources * **Bedrock pricing:** [Amazon Bedrock pricing page](https://aws.amazon.com/bedrock/pricing/) * **AWS pricing documentation:** [Bedrock pricing guide](https://docs.aws.amazon.com/bedrock/latest/userguide/bedrock-pricing.html) * **AWS blog post:** [Introducing Claude Sonnet 4.5 in Amazon Bedrock](https://aws.amazon.com/blogs/aws/introducing-claude-sonnet-4-5-in-amazon-bedrock-anthropics-most-intelligent-model-best-for-coding-and-complex-agents/) * **Anthropic pricing details:** [Cloud platform pricing](https://platform.claude.com/docs/en/about-claude/pricing#cloud-platform-pricing) --- title: Claude on Google Cloud url: https://platform.claude.com/docs/en/build-with-claude/claude-on-vertex-ai description: Anthropic's Claude models are available through [Google Cloud's Agent Platform](https://cloud.google.com/vertex-ai). --- The API for accessing Claude on Google Cloud's Agent Platform is nearly identical to the [Messages API](https://platform.claude.com/docs/en/api/messages/create), with two key differences in request format: * On Agent Platform, `model` is not passed in the request body. Instead, it is specified in the Google Cloud endpoint URL. * On Agent Platform, `anthropic_version` is passed in the request body (rather than as a header), and must be set to the value `vertex-2023-10-16`. Agent Platform is also supported by Anthropic's official [client SDKs](https://platform.claude.com/docs/en/cli-sdks-libraries/overview). This guide walks you through making a request to Claude on Agent Platform using one of Anthropic's client SDKs. Note that this guide assumes you already have a Google Cloud project that is able to use Agent Platform. See [Anthropic Claude models on Agent Platform](https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/partner-models/claude) for more information on the setup required and a full walkthrough. ## Install an SDK for accessing Agent Platform First, install Anthropic's [client SDK](https://platform.claude.com/docs/en/cli-sdks-libraries/overview) for your language of choice. ```bash pip install -U "anthropic[vertex]" ``` ```bash npm install @anthropic-ai/vertex-sdk ``` ```bash dotnet add package Anthropic.Vertex ``` ```bash go get github.com/anthropics/anthropic-sdk-go ``` ```groovy Gradle implementation("com.anthropic:anthropic-java:2.53.0") implementation("com.anthropic:anthropic-java-vertex:2.53.0") ``` ```xml Maven com.anthropic anthropic-java 2.53.0 com.anthropic anthropic-java-vertex 2.53.0 ``` ```java Java import com.anthropic.client.AnthropicClient; import com.anthropic.client.okhttp.AnthropicOkHttpClient; import com.anthropic.models.messages.Message; import com.anthropic.models.messages.MessageCreateParams; import com.anthropic.models.messages.Model; import com.anthropic.vertex.backends.VertexBackend; void main() { AnthropicClient client = AnthropicOkHttpClient.builder() .backend(VertexBackend.fromEnv()) .build(); MessageCreateParams params = MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .addUserMessage("What is the capital of France?") .build(); Message response = client.messages().create(params); response.content().stream() .flatMap(block -> block.text().stream()) .forEach(textBlock -> IO.println(textBlock.text())); } ``` ```bash composer require anthropic-ai/sdk google/auth ``` ```bash # Gemfile gem "anthropic" gem "googleauth" ``` ## Accessing Agent Platform ### Model availability Note that Anthropic model availability varies by region. Search for "Claude" in the [Model Garden](https://cloud.google.com/model-garden) or go to [Anthropic Claude models](https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/partner-models/claude) for the latest information. #### API model IDs Lifecycle terms (Deprecated, Retired) are defined in [Model deprecations](https://platform.claude.com/docs/en/about-claude/model-deprecations). Lifecycle dates on partner-operated platforms are set by the partner and can differ from the Claude API schedule. For the current retirement date of any model on Agent Platform, see [Google Cloud's documentation for Claude models on Agent Platform](https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/partner-models/claude). | Model | Agent Platform API model ID | | ---------------------------- | --------------------------- | | Claude Fable 5 | claude-fable-5 | | Claude Opus 5 | claude-opus-5 | | Claude Opus 4.8 | claude-opus-4-8 | | Claude Opus 4.7 | claude-opus-4-7 | | Claude Opus 4.6 | claude-opus-4-6 | | Claude Sonnet 5 | `claude-sonnet-5` | | Claude Sonnet 4.6 | claude-sonnet-4-6 | | Claude Sonnet 4.5 | claude-sonnet-4-5\@20250929 | | Claude Sonnet 4 Deprecated. | claude-sonnet-4\@20250514 | | Claude Sonnet 3.7 Retired. | claude-3-7-sonnet\@20250219 | | Claude Opus 4.5 | claude-opus-4-5\@20251101 | | Claude Opus 4.1 Deprecated. | claude-opus-4-1\@20250805 | | Claude Opus 4 Deprecated. | claude-opus-4\@20250514 | | Claude Haiku 4.5 | claude-haiku-4-5\@20251001 | | Claude Haiku 3.5 Deprecated. | claude-3-5-haiku\@20241022 | Upgrading to a newer Claude model? In Claude Code, run `/claude-api migrate` to apply model ID swaps and breaking parameter changes across your codebase. The skill detects which cloud platform your code targets and adjusts model ID formats and feature changes for that platform. See [Migrating to a newer Claude model](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/claude-api-skill#migrating-to-a-newer-claude-model). ### Making requests Before running requests you might need to run `gcloud auth application-default login` to authenticate with Google Cloud. The following examples show how to generate text from Claude on Agent Platform: ```bash cURL MODEL_ID=claude-opus-5 PROJECT_ID=MY_PROJECT_ID curl https://aiplatform.googleapis.com/v1/projects/${PROJECT_ID}/locations/global/publishers/anthropic/models/${MODEL_ID}:rawPredict \ -H "Authorization: Bearer $(gcloud auth print-access-token)" \ -H "Content-Type: application/json" \ -d '{ "anthropic_version": "vertex-2023-10-16", "messages": [{"role": "user", "content": "Hey Claude!"}], "max_tokens": 100 }' ``` ```bash CLI # The ant CLI does not support Agent Platform. ``` ```python Python from anthropic import AnthropicVertex project_id = "MY_PROJECT_ID" region = "global" client = AnthropicVertex(project_id=project_id, region=region) message = client.messages.create( model="claude-opus-5", max_tokens=100, messages=[ { "role": "user", "content": "Hey Claude!", } ], ) print(message) ``` ```typescript TypeScript import { AnthropicVertex } from "@anthropic-ai/vertex-sdk"; const projectId = "MY_PROJECT_ID"; const region = "global"; // Goes through the standard `google-auth-library` flow. const client = new AnthropicVertex({ projectId, region }); const result = await client.messages.create({ model: "claude-opus-5", max_tokens: 100, messages: [ { role: "user", content: "Hey Claude!" } ] }); console.log(JSON.stringify(result, null, 2)); ``` ```csharp C# using Anthropic.Models.Messages; using Anthropic.Vertex; var projectId = "MY_PROJECT_ID"; var region = "global"; var client = new AnthropicVertexClient(new AnthropicVertexCredentials(region, projectId)); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 100, Messages = [new() { Role = Role.User, Content = "Hey Claude!" }] }; var message = await client.Messages.Create(parameters); Console.WriteLine(message); ``` ```go Go import ( "context" "fmt" "github.com/anthropics/anthropic-sdk-go" "github.com/anthropics/anthropic-sdk-go/vertex" ) // ... // Uses default Google Cloud credentials client := anthropic.NewClient( vertex.WithGoogleAuth(context.Background(), "global", "MY_PROJECT_ID"), ) message, err := client.Messages.New(context.Background(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 100, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Hey Claude!")), }, }) if err != nil { panic(err) } fmt.Printf("%+v\n", message) ``` ```java Java import com.anthropic.client.AnthropicClient; import com.anthropic.client.okhttp.AnthropicOkHttpClient; import com.anthropic.models.messages.Message; import com.anthropic.models.messages.MessageCreateParams; import com.anthropic.models.messages.Model; import com.anthropic.vertex.backends.VertexBackend; void main() { // Uses default Google Cloud credentials AnthropicClient client = AnthropicOkHttpClient.builder() .backend(VertexBackend.fromEnv()) .build(); Message message = client .messages() .create( MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(100) .addUserMessage("Hey Claude!") .build() ); IO.println(message); } ``` ```php PHP messages->create( maxTokens: 100, messages: [ ['role' => 'user', 'content' => 'Hey Claude!'] ], model: 'claude-opus-5', ); $textBlock = array_find($message->content, static fn ($block): bool => $block->type === 'text'); echo $textBlock->text; ``` ```ruby Ruby require "anthropic" client = Anthropic::VertexClient.new( region: "global", project_id: "MY_PROJECT_ID" ) message = client.messages.create( model: "claude-opus-5", max_tokens: 100, messages: [{role: "user", content: "Hey Claude!"}] ) puts message.content.find { it.type == :text }.text ``` See the [client SDKs](https://platform.claude.com/docs/en/cli-sdks-libraries/overview) and the official [Agent Platform docs](https://cloud.google.com/vertex-ai/docs) for more details. Claude is also available through [Amazon Bedrock](https://platform.claude.com/docs/en/build-with-claude/claude-in-amazon-bedrock), [Claude Platform on AWS](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws), and [Microsoft Foundry](https://platform.claude.com/docs/en/build-with-claude/claude-in-microsoft-foundry). ## Data retention Data handling for this offering is governed by Google Cloud. For details, see [Agent Platform and zero data retention](https://cloud.google.com/vertex-ai/generative-ai/docs/data-governance). ## Activity logging Agent Platform provides a [request-response logging service](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/request-response-logging) that allows you to log the prompts and completions associated with your usage. Anthropic recommends that you log your activity on at least a 30-day rolling basis to understand your activity and investigate any potential misuse. Turning on this service does not give Google or Anthropic any access to your content. ## Feature support For the full feature list with Google Cloud availability, see [Features overview](https://platform.claude.com/docs/en/build-with-claude/overview). ### Supported feature highlights * [Messages API](https://platform.claude.com/docs/en/api/messages/create) * [Prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) * [Thinking](https://platform.claude.com/docs/en/build-with-claude/thinking) * [Tool use](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview), including the [Bash tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/bash-tool), [Computer use tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/computer-use-tool), [Memory tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/memory-tool), and [Text editor tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/text-editor-tool) * [Web search tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-search-tool) * [Citations](https://platform.claude.com/docs/en/build-with-claude/citations) * [Structured outputs](https://platform.claude.com/docs/en/build-with-claude/structured-outputs) ### Features not supported * Input sources (URL sources for images and documents, Files API) * Server-side tools (code execution, web fetch, advisor) * Agent infrastructure (Agent Skills, MCP connector, programmatic tool calling) * API endpoints (Message Batches, Models, Admin, Compliance, Usage and Cost) * Claude Managed Agents * Server-side fallback (the [`fallbacks` parameter](https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback#server-side-fallback); use the [client-side fallback pattern](https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback#client-side-fallback) instead) ### Context window Claude Fable 5, Claude Opus 5, Claude Opus 4.8, Claude Opus 4.7, Claude Opus 4.6, Claude Sonnet 5, and Claude Sonnet 4.6 have a [1M-token context window](https://platform.claude.com/docs/en/build-with-claude/context-windows) on Agent Platform. Other Claude models, including Sonnet 4.5 and Sonnet 4 (deprecated), have a 200k-token context window. Agent Platform limits request payloads to 30 MB. When sending large documents or many images, you might reach this limit before the token limit. ## Global, multi-region, and regional endpoints Agent Platform offers three endpoint types: * **Global endpoints:** Dynamic routing for maximum availability * **Multi-region endpoints:** Dynamic routing within a geographic area (for example, the United States or the European Union) for data residency with high availability * **Regional endpoints:** Guaranteed data routing through specific geographic regions Regional and multi-region endpoints include a 10% pricing premium over global endpoints. This applies to Claude Sonnet 4.5 and future models only. Older models (Claude Sonnet 4 (deprecated), Opus 4 (deprecated), and earlier) maintain their existing pricing structures. ### When to use each option **Global endpoints (recommended):** * Provide maximum availability and uptime * Dynamically route requests to regions with available capacity * No pricing premium * Best for applications where data residency is flexible * Only supports pay-as-you-go traffic (provisioned throughput requires regional endpoints) **Multi-region endpoints:** * Dynamically route requests across regions within a geographic area (currently `us` and `eu`) * Useful when you need data residency within a broad geography but want higher availability than a single region * 10% pricing premium over global endpoints * Only supports pay-as-you-go traffic (provisioned throughput requires regional endpoints) **Regional endpoints:** * Route traffic through specific geographic regions * Required for single-region data residency, strict compliance mandates, or provisioned throughput * Support both pay-as-you-go and provisioned throughput * 10% pricing premium reflects infrastructure costs for dedicated regional capacity ### Implementation **Using global endpoints (recommended):** Set the `region` parameter to `"global"` when initializing the client: ```bash cURL MODEL_ID=claude-opus-5 PROJECT_ID=MY_PROJECT_ID curl https://aiplatform.googleapis.com/v1/projects/${PROJECT_ID}/locations/global/publishers/anthropic/models/${MODEL_ID}:rawPredict \ -H "Authorization: Bearer $(gcloud auth print-access-token)" \ -H "Content-Type: application/json" \ -d '{ "anthropic_version": "vertex-2023-10-16", "messages": [{"role": "user", "content": "Hey Claude!"}], "max_tokens": 100 }' ``` ```bash CLI # The ant CLI does not support Agent Platform. ``` ```python Python from anthropic import AnthropicVertex project_id = "MY_PROJECT_ID" region = "global" client = AnthropicVertex(project_id=project_id, region=region) message = client.messages.create( model="claude-opus-5", max_tokens=100, messages=[ { "role": "user", "content": "Hey Claude!", } ], ) print(message) ``` ```typescript TypeScript import { AnthropicVertex } from "@anthropic-ai/vertex-sdk"; const projectId = "MY_PROJECT_ID"; const region = "global"; const client = new AnthropicVertex({ projectId, region }); const result = await client.messages.create({ model: "claude-opus-5", max_tokens: 100, messages: [ { role: "user", content: "Hey Claude!" } ] }); console.log(JSON.stringify(result, null, 2)); ``` ```csharp C# using Anthropic.Models.Messages; using Anthropic.Vertex; var projectId = "MY_PROJECT_ID"; var region = "global"; var client = new AnthropicVertexClient(new AnthropicVertexCredentials(region, projectId)); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 100, Messages = [new() { Role = Role.User, Content = "Hey Claude!" }] }; var message = await client.Messages.Create(parameters); Console.WriteLine(message); ``` ```go Go import ( "context" "fmt" "github.com/anthropics/anthropic-sdk-go" "github.com/anthropics/anthropic-sdk-go/vertex" ) // ... // Uses default Google Cloud credentials client := anthropic.NewClient( vertex.WithGoogleAuth(context.Background(), "global", "MY_PROJECT_ID"), ) message, err := client.Messages.New(context.Background(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 100, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Hey Claude!")), }, }) if err != nil { panic(err) } fmt.Printf("%+v\n", message) ``` ```java Java import com.anthropic.client.AnthropicClient; import com.anthropic.client.okhttp.AnthropicOkHttpClient; import com.anthropic.models.messages.MessageCreateParams; import com.anthropic.models.messages.Model; import com.anthropic.vertex.backends.VertexBackend; import com.google.auth.oauth2.GoogleCredentials; void main() throws Exception { // Uses default Google Cloud credentials AnthropicClient client = AnthropicOkHttpClient.builder() .backend( VertexBackend.builder() .googleCredentials(GoogleCredentials.getApplicationDefault()) .region("global") .project("MY_PROJECT_ID") .build() ) .build(); var message = client .messages() .create( MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(100) .addUserMessage("Hey Claude!") .build() ); IO.println(message); } ``` ```php PHP messages->create( maxTokens: 100, messages: [ ['role' => 'user', 'content' => 'Hey Claude!'] ], model: 'claude-opus-5', ); $textBlock = array_find($message->content, static fn ($block): bool => $block->type === 'text'); echo $textBlock->text; ``` ```ruby Ruby require "anthropic" client = Anthropic::VertexClient.new( region: "global", project_id: "MY_PROJECT_ID" ) message = client.messages.create( model: "claude-opus-5", max_tokens: 100, messages: [{role: "user", content: "Hey Claude!"}] ) puts message.content.find { it.type == :text }.text ``` **Using multi-region endpoints:** Set the `region` parameter to a multi-region identifier: `"us"` for the United States or `"eu"` for the European Union. The SDK routes requests to the corresponding multi-region endpoint (`https://aiplatform.us.rep.googleapis.com` or `https://aiplatform.eu.rep.googleapis.com`), which dynamically balances traffic across regions within that geography. ```bash cURL MODEL_ID=claude-opus-5 LOCATION=us # Multi-region identifier: "us" or "eu" PROJECT_ID=MY_PROJECT_ID curl https://aiplatform.${LOCATION}.rep.googleapis.com/v1/projects/${PROJECT_ID}/locations/${LOCATION}/publishers/anthropic/models/${MODEL_ID}:rawPredict \ -H "Authorization: Bearer $(gcloud auth print-access-token)" \ -H "Content-Type: application/json" \ -d '{ "anthropic_version": "vertex-2023-10-16", "messages": [{"role": "user", "content": "Hey Claude!"}], "max_tokens": 100 }' ``` ```bash CLI # The ant CLI does not support Agent Platform. ``` ```python Python from anthropic import AnthropicVertex project_id = "MY_PROJECT_ID" region = "us" # Multi-region identifier: "us" or "eu" client = AnthropicVertex(project_id=project_id, region=region) message = client.messages.create( model="claude-opus-5", max_tokens=100, messages=[ { "role": "user", "content": "Hey Claude!", } ], ) print(message) ``` ```typescript TypeScript import { AnthropicVertex } from "@anthropic-ai/vertex-sdk"; const projectId = "MY_PROJECT_ID"; const region = "us"; // Multi-region identifier: "us" or "eu" const client = new AnthropicVertex({ projectId, region }); const result = await client.messages.create({ model: "claude-opus-5", max_tokens: 100, messages: [ { role: "user", content: "Hey Claude!" } ] }); console.log(JSON.stringify(result, null, 2)); ``` ```csharp C# using Anthropic.Models.Messages; using Anthropic.Vertex; var projectId = "MY_PROJECT_ID"; var region = "us"; // Multi-region identifier: "us" or "eu" var client = new AnthropicVertexClient(new AnthropicVertexCredentials(region, projectId)); var parameters = new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 100, Messages = [new() { Role = Role.User, Content = "Hey Claude!" }] }; var message = await client.Messages.Create(parameters); Console.WriteLine(message); ``` ```go Go import ( "context" "fmt" "github.com/anthropics/anthropic-sdk-go" "github.com/anthropics/anthropic-sdk-go/vertex" ) // ... // Multi-region identifier: "us" or "eu" client := anthropic.NewClient( vertex.WithGoogleAuth(context.Background(), "us", "MY_PROJECT_ID"), ) message, err := client.Messages.New(context.Background(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 100, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Hey Claude!")), }, }) if err != nil { panic(err) } fmt.Printf("%+v\n", message) ``` ```java Java import com.anthropic.client.AnthropicClient; import com.anthropic.client.okhttp.AnthropicOkHttpClient; import com.anthropic.models.messages.MessageCreateParams; import com.anthropic.models.messages.Model; import com.anthropic.vertex.backends.VertexBackend; import com.google.auth.oauth2.GoogleCredentials; void main() throws Exception { // Multi-region identifier: "us" or "eu" AnthropicClient client = AnthropicOkHttpClient.builder() .backend( VertexBackend.builder() .googleCredentials(GoogleCredentials.getApplicationDefault()) .region("us") .project("MY_PROJECT_ID") .build() ) .build(); var message = client .messages() .create( MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(100) .addUserMessage("Hey Claude!") .build() ); IO.println(message); } ``` ```php PHP messages->create( maxTokens: 100, messages: [ ['role' => 'user', 'content' => 'Hey Claude!'] ], model: 'claude-opus-5', ); $textBlock = array_find($message->content, static fn ($block): bool => $block->type === 'text'); echo $textBlock->text; ``` ```ruby Ruby require "anthropic" client = Anthropic::VertexClient.new( region: "us", # Multi-region identifier: "us" or "eu" project_id: "MY_PROJECT_ID" ) message = client.messages.create( model: "claude-opus-5", max_tokens: 100, messages: [{role: "user", content: "Hey Claude!"}] ) puts message.content.find { it.type == :text }.text ``` **Using regional endpoints:** Specify a specific region such as `"us-east5"` or `"europe-west1"`: ```bash cURL # Specific regional endpoints support Claude Sonnet 4.6 and earlier; newer models use the global or multi-region endpoints MODEL_ID=claude-sonnet-4-6 LOCATION=us-east5 # Specify a specific region PROJECT_ID=MY_PROJECT_ID curl https://${LOCATION}-aiplatform.googleapis.com/v1/projects/${PROJECT_ID}/locations/${LOCATION}/publishers/anthropic/models/${MODEL_ID}:rawPredict \ -H "Authorization: Bearer $(gcloud auth print-access-token)" \ -H "Content-Type: application/json" \ -d '{ "anthropic_version": "vertex-2023-10-16", "messages": [{"role": "user", "content": "Hey Claude!"}], "max_tokens": 100 }' ``` ```bash CLI # The ant CLI does not support Agent Platform. ``` ```python Python from anthropic import AnthropicVertex project_id = "MY_PROJECT_ID" region = "us-east5" # Specify a specific region client = AnthropicVertex(project_id=project_id, region=region) message = client.messages.create( # Specific regional endpoints support Claude Sonnet 4.6 and earlier; newer models use the global or multi-region endpoints model="claude-sonnet-4-6", max_tokens=100, messages=[ { "role": "user", "content": "Hey Claude!", } ], ) print(message) ``` ```typescript TypeScript import { AnthropicVertex } from "@anthropic-ai/vertex-sdk"; const projectId = "MY_PROJECT_ID"; const region = "us-east5"; // Specify a specific region const client = new AnthropicVertex({ projectId, region }); const result = await client.messages.create({ // Specific regional endpoints support Claude Sonnet 4.6 and earlier; newer models use the global or multi-region endpoints model: "claude-sonnet-4-6", max_tokens: 100, messages: [ { role: "user", content: "Hey Claude!" } ] }); console.log(JSON.stringify(result, null, 2)); ``` ```csharp C# using Anthropic.Models.Messages; using Anthropic.Vertex; var projectId = "MY_PROJECT_ID"; var region = "us-east5"; // Specify a specific region var client = new AnthropicVertexClient(new AnthropicVertexCredentials(region, projectId)); var parameters = new MessageCreateParams { // Specific regional endpoints support Claude Sonnet 4.6 and earlier; newer models use the global or multi-region endpoints Model = Model.ClaudeSonnet4_6, MaxTokens = 100, Messages = [new() { Role = Role.User, Content = "Hey Claude!" }] }; var message = await client.Messages.Create(parameters); Console.WriteLine(message); ``` ```go Go import ( "context" "fmt" "github.com/anthropics/anthropic-sdk-go" "github.com/anthropics/anthropic-sdk-go/vertex" ) // ... // Specify a specific region client := anthropic.NewClient( vertex.WithGoogleAuth(context.Background(), "us-east5", "MY_PROJECT_ID"), ) message, err := client.Messages.New(context.Background(), anthropic.MessageNewParams{ // Specific regional endpoints support Claude Sonnet 4.6 and earlier; newer models use the global or multi-region endpoints Model: anthropic.ModelClaudeSonnet4_6, MaxTokens: 100, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Hey Claude!")), }, }) if err != nil { panic(err) } fmt.Printf("%+v\n", message) ``` ```java Java import com.anthropic.client.AnthropicClient; import com.anthropic.client.okhttp.AnthropicOkHttpClient; import com.anthropic.models.messages.MessageCreateParams; import com.anthropic.models.messages.Model; import com.anthropic.vertex.backends.VertexBackend; import com.google.auth.oauth2.GoogleCredentials; void main() throws Exception { // Uses default Google Cloud credentials with specific region AnthropicClient client = AnthropicOkHttpClient.builder() .backend( VertexBackend.builder() .googleCredentials(GoogleCredentials.getApplicationDefault()) .region("us-east5") // Specify a specific region .project("MY_PROJECT_ID") .build() ) .build(); var message = client .messages() .create( MessageCreateParams.builder() // Specific regional endpoints support Claude Sonnet 4.6 and earlier; newer models use the global or multi-region endpoints .model(Model.CLAUDE_SONNET_4_6) .maxTokens(100) .addUserMessage("Hey Claude!") .build() ); IO.println(message); } ``` ```php PHP messages->create( maxTokens: 100, messages: [ ['role' => 'user', 'content' => 'Hey Claude!'] ], // Specific regional endpoints support Claude Sonnet 4.6 and earlier; newer models use the global or multi-region endpoints model: 'claude-sonnet-4-6', ); echo $message->content[0]->text; ``` ```ruby Ruby require "anthropic" client = Anthropic::VertexClient.new( region: "us-east5", # Specify a specific region project_id: "MY_PROJECT_ID" ) message = client.messages.create( # Specific regional endpoints support Claude Sonnet 4.6 and earlier; newer models use the global or multi-region endpoints model: "claude-sonnet-4-6", max_tokens: 100, messages: [{role: "user", content: "Hey Claude!"}] ) puts message.content.first.text ``` Claude Mythos Preview is a research preview available to invited customers on Agent Platform. For more information, see [Project Glasswing](https://anthropic.com/glasswing). ## Additional resources * **Agent Platform pricing:** [Generative AI pricing on cloud.google.com](https://cloud.google.com/vertex-ai/generative-ai/pricing) * **Claude models documentation:** [Claude on Agent Platform](https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/partner-models/claude) * **Google blog post:** [Global endpoint for Claude models](https://cloud.google.com/blog/products/ai-machine-learning/global-endpoint-for-claude-models-generally-available-on-vertex-ai) * **Anthropic pricing details:** [Cloud platform pricing](https://platform.claude.com/docs/en/about-claude/pricing#cloud-platform-pricing) --- title: Claude Platform on AWS url: https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws description: Access Claude's full platform capabilities through AWS with Anthropic-managed infrastructure. --- Claude Platform on AWS gives you the full Anthropic platform experience, including the Messages API, Agent Skills, code execution, and beta features, accessible through your AWS account. Unlike [Amazon Bedrock](https://platform.claude.com/docs/en/build-with-claude/claude-in-amazon-bedrock), where AWS operates the inference stack, Anthropic operates Claude Platform on AWS. AWS provides the authentication layer (SigV4 or API key), IAM-based access control, and billing integration through AWS Marketplace. The Anthropic SDKs support Claude Platform on AWS. ## How the platform integration works Claude models run on Anthropic-managed infrastructure. This is a commercial integration for billing and access through AWS. Anthropic is the data processor for inference inputs and outputs. AWS processes billing and identity metadata under the marketplace model. Customers using Claude through Claude Platform on AWS are subject to Anthropic's [data use terms](https://www.anthropic.com/legal). Claude Platform on AWS has the following operational characteristics: data may not reside in AWS, inference may route to Anthropic's primary cloud, and subservices may change without notice. Set the [`inference_geo`](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#data-residency) parameter per request to pin inference to a specific geography. Claude Platform on AWS follows the same data retention policy as the first-party Claude API. Zero Data Retention (ZDR) is available on request. Contact your Anthropic account representative to enable it for your organization. ## Claude Platform on AWS vs Amazon Bedrock Both offerings let you use Claude through AWS, but they differ in architecture, API surface, and feature availability. | Aspect | Claude Platform on AWS | [Claude in Amazon Bedrock](https://platform.claude.com/docs/en/build-with-claude/claude-in-amazon-bedrock) | [Amazon Bedrock (Opus 4.6 and earlier)](https://platform.claude.com/docs/en/build-with-claude/claude-on-amazon-bedrock-legacy) | | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | **Who operates the stack** | Anthropic | AWS | AWS | | **API surface** | Claude API (`/v1/{endpoint}`) | Messages API at `/anthropic/v1/messages` | Bedrock Converse / InvokeModel | | **Feature availability** | Typically same-day as Claude API (see [feature limitations](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#features-not-supported)) | Per Amazon Bedrock release schedule | Per Amazon Bedrock release schedule | | **Agent Skills** | Available (beta) | Not available (requires code execution) | Not available | | **Beta features** | Pass through with `anthropic-beta` headers (see [feature limitations](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#features-not-supported)) | `anthropic-beta` header not supported | `anthropic-beta` header not supported | | **Authentication** | AWS IAM / SigV4 or API key | AWS IAM / SigV4 | AWS IAM / SigV4 or bearer token | | **Billing** | AWS Marketplace | AWS (native service) | AWS (native service) | | **Base URL** | `aws-external-anthropic.{region}.api.aws` | `bedrock-mantle.{region}.api.aws` | `bedrock-runtime.{region}.amazonaws.com` | | **SDK client** | Platform-specific client class (for example, `AnthropicAWS` in Python), in beta | `AnthropicBedrockMantle` | `AnthropicBedrock` / Bedrock SDK | | **Console** | Claude Console (`platform.claude.com`, access through the AWS Console) | Bedrock Console | Bedrock Console | | **Rate limits and quotas** | Managed by Anthropic | Managed by AWS | Managed by AWS | | **Inference data processor** | Anthropic | AWS | AWS | If you need AWS-operated Claude, see [Claude in Amazon Bedrock](https://platform.claude.com/docs/en/build-with-claude/claude-in-amazon-bedrock). Claude Platform on AWS uses a separate capacity pool from both the first-party Claude API and Amazon Bedrock. You can run workloads on more than one platform and fail over between them. [AWS PrivateLink](https://docs.aws.amazon.com/vpc/latest/privatelink/what-is-privatelink.html) is supported for connecting your VPC to the Claude Platform on AWS endpoint. **When to choose Bedrock:** Organizations in regulated industries that require FedRAMP High, IL4, IL5, or HIPAA-ready compliance, or that need AWS to be the sole data processor, should use [Claude in Amazon Bedrock](https://platform.claude.com/docs/en/build-with-claude/claude-in-amazon-bedrock). Bedrock runs entirely on AWS-controlled infrastructure with AWS as the operating party. **Which offering are you using?** Claude is available through several distinct products: * **Claude Platform on AWS** (this page): The Claude API platform billed through AWS Marketplace. Managed in the [Claude Console](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#using-the-claude-console) and the AWS Console. * **[Claude in Amazon Bedrock](https://platform.claude.com/docs/en/build-with-claude/claude-in-amazon-bedrock):** An AWS-native service. Managed in the Amazon Bedrock console and billed as AWS service usage. * **Claude Enterprise procured through AWS Marketplace:** A [claude.ai](https://claude.ai) plan (the Claude chat product), not an API platform. Managed at claude.ai, and its account and migration behavior differ from what this page describes. See the [Claude Help Center](https://support.claude.com). * **Direct Anthropic accounts:** The first-party Claude API and claude.ai plans billed by Anthropic. Managed in the Claude Console and at claude.ai. ## Set up your account Setting up Claude Platform on AWS happens in four phases: sign up on the AWS Console service page, complete your Anthropic organization setup, note your workspace ID, and sign in to the Claude Console. Signing up through the AWS Console provisions a new Anthropic organization tied to your AWS account. This organization is separate from any existing organizations your company has with Anthropic, including Claude Enterprise organizations procured through AWS Marketplace. API keys, workspaces, and Claude Console settings from a first-party Anthropic organization don't carry over. If you have an existing Amazon Bedrock private offer, contact your Anthropic or AWS account representative before signing up so your discount applies from your first request. Discounts cannot be applied retroactively to usage incurred before your private offer is accepted. See [Private offers](https://platform.claude.com/docs/en/about-claude/pricing#private-offers). 1. Open the [AWS Console](https://console.aws.amazon.com/) and navigate to the **Claude Platform on AWS** service page. 2. Choose **Sign up**. 3. On the Sign-up page, review the terms (Anthropic's End User License Agreement, the AWS Privacy Notice, and the AWS Customer Agreement) and select the agreement checkbox. 4. Choose **Continue**. The page shows a **Sign-up in progress** banner. Stay on the page. Sign-up takes a few minutes while AWS handles the AWS Marketplace subscription for you, then redirects you automatically. If your organization has a private offer from Anthropic, the Console looks it up and prompts you to accept it in AWS Marketplace. See [Private offers](https://platform.claude.com/docs/en/about-claude/pricing#private-offers) for details. If you use Claude Platform on AWS, your content (such as prompts and completions) is processed by Anthropic outside of AWS. See Anthropic's [data use policies](https://www.anthropic.com/legal) for details on how content and metadata are processed and stored. After sign-up completes, you're redirected to `platform.claude.com/partner-signup`. 1. Enter the email address of your organization's owner and choose **Get started**. 2. Check that email inbox for a setup link and follow it. If your browser shows a **Signed in as a different account** page, choose **Log out and continue**. 3. Complete the organization details form (organization name, entity type, country, intended use) and choose **Complete setup**. Completing setup creates your Anthropic organization and accepts Anthropic's Commercial Terms of Service and Usage Policy. The AWS Console service page now shows a left navigation with **Home**, **API keys**, **Quickstart**, and **Workspaces**. After you complete setup, the AWS Console prompts you to create a workspace. See [Workspaces](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#workspaces) for details on region binding, IAM resource scoping, and creating additional workspaces. Find the workspace ID under **Workspaces** on the AWS Console **Claude Platform on AWS** service page or in the [Claude Console](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#using-the-claude-console). Workspace IDs use the format `wrkspc_` followed by an alphanumeric identifier. Access to the Claude Console is federated through AWS IAM: 1. Assume an IAM role with the `aws-external-anthropic:AssumeConsole` permission. See [IAM actions for Claude Platform on AWS](https://platform.claude.com/docs/en/api/claude-platform-on-aws-iam-actions#console-access). 2. From the **Claude Platform on AWS** service page, choose **Open Claude Console**. The AWS Console issues a JWT and redirects you to `platform.claude.com`. 3. On first sign-in, you're prompted for an email address. Enter your work email. The platform provisions your Claude Console user just-in-time. When you're signed in through the AWS Console, the Claude Console scopes to your Claude Platform on AWS organization. An **Account managed by AWS** indicator appears in the bottom-left of the Claude Console sidebar. ### Moving from an existing Anthropic organization Signing up for Claude Platform on AWS always provisions a new Anthropic organization tied to your AWS account. There is no in-place conversion: an existing organization, such as a first-party Claude API organization, can't become a Claude Platform on AWS organization. Plan a move from an existing organization as a cutover to a new one: * **Create the new organization first.** Sign up through the AWS Console (see [Set up your account](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#set-up-your-account)). If your move involves a private offer, complete sign-up before the offer is accepted: discounts apply from acceptance, not retroactively. See [Private offers](https://platform.claude.com/docs/en/about-claude/pricing#private-offers). * **Recreate access and configuration.** API keys, workspaces, and Claude Console settings don't carry over from an existing organization. Create workspaces in the new organization and switch your applications to [Claude Platform on AWS authentication](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#authentication). * **Update your integration.** Claude Platform on AWS serves the Claude API (`/v1/{endpoint}`), so request and response shapes are unchanged from the first-party Claude API. What changes is the base URL, the authentication method, and the required `anthropic-workspace-id` header; see [Making requests](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#making-requests). Some platform features differ; see [Features not supported](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#features-not-supported). * **Cut over on your own schedule.** The new organization is independent of your existing one, and both can serve traffic in parallel. There's no need for a hard cutover: shift workloads gradually until all of your traffic is on the new organization. Once the new organization is running, the differences are concentrated in billing and authentication, which are handled through AWS: * **Billing** moves to AWS Marketplace: usage is billed in Claude Consumption Units rather than prepaid credits (see [Billing](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#billing)), and spend limits are managed on the Billing page rather than the Limits page (see [Spend limits](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#spend-limits)). During the transition, billing stays separate: the existing organization continues to be billed as it is today. * **Authentication and access** move to AWS: requests authenticate with AWS credentials or with API keys generated in the AWS Console, not the Claude Console (see [Authentication](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#authentication)). Organization membership is managed through AWS IAM rather than the Claude Console (see [Available pages](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#available-pages)), and Anthropic's client SDKs provide platform-specific client classes (see [Install an SDK](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#install-an-sdk)). * **Day-to-day API usage** works the way it does on the first-party Claude API, except where noted in the [feature limitations](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#features-not-supported). Before shifting production traffic, check your rate limits: new organizations are placed on the Start tier, and limit increases go through your Anthropic account representative (see [Rate limits and quotas](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#rate-limits-and-quotas)). For Claude Enterprise (claude.ai) organizations, which behave differently, see the [offering comparison](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#claude-platform-on-aws-vs-amazon-bedrock). ### Troubleshooting account setup * **"Sign-up failed: Failed to enable OutboundWebIdentityFederation":** If you see this banner on first submit, choose **Continue** again. The IAM enablement can take a moment to take effect. * **No progress indicator during sign-up:** Sign-up takes a few minutes. The page shows a static **Sign-up in progress** banner without a progress bar while AWS provisions your account. * **"Signed in as a different account" after following the setup link:** Choose **Log out and continue**. The page reauthenticates you with the email address you entered. * **"Not found" message during sign-in:** This message might appear briefly during redirect. You can dismiss it. * **Usage page shows no data after your first API call:** Usage data can take a few minutes to appear in the Claude Console. * **"Outbound web identity federation is disabled" on your first API call:** Enable federation once per account. See [Enable outbound web identity federation](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#enable-outbound-web-identity-federation). ## Before making API calls Ensure you have: 1. An active AWS account with a subscription to Claude Platform on AWS (see [Set up your account](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#set-up-your-account)) 2. The [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-welcome.html) installed and configured 3. **Outbound web identity federation enabled** on your AWS account, a one-time setup step (see [Enable outbound web identity federation](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#enable-outbound-web-identity-federation)) 4. Your workspace ID (see [Obtain your workspace ID](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#obtain-your-workspace-id)) 5. IAM permission to call the API: the `aws-external-anthropic:CreateInference` action on your workspace, plus `aws-external-anthropic:CallWithBearerToken` if you authenticate with an API key (see [IAM policies](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#iam-policies)) ### Enable outbound web identity federation The Claude Platform on AWS gateway calls `sts:GetWebIdentityToken` server-side to mint a JWT it forwards to Anthropic. This STS capability is **disabled by default** on every AWS account. Enable it once per account: ```bash CLI aws iam enable-outbound-web-identity-federation ``` If the response is `[ERROR] (FeatureEnabled) ... already enabled`, the setting is already on for your account and you can move on. Verify and retrieve your account's issuer URL: ```bash CLI aws iam get-outbound-web-identity-federation-info ``` Without this step, every request returns `"Outbound web identity federation is disabled for your account"`. This is the most common setup error. ### Obtain your workspace ID You create a workspace from the AWS Console after completing account setup (see [Set up your account](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#set-up-your-account)). Workspaces are bound to a single AWS region. You can find the workspace ID in the [Claude Console](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#using-the-claude-console) under **Workspaces** or in the **Workspaces** section of the AWS Console service page. Set the `ANTHROPIC_AWS_WORKSPACE_ID` and `AWS_REGION` environment variables so the SDK clients read them automatically: ```bash CLI export ANTHROPIC_AWS_WORKSPACE_ID='wrkspc_01AbCdEf23GhIj' export AWS_REGION='us-west-2' # Your workspace's AWS region ``` The region is required. The SDK client raises an error if no region is set. Pass `aws_region`/`awsRegion` to the constructor, or set `AWS_REGION` (or `AWS_DEFAULT_REGION`). All AWS commercial regions are supported. ## Authentication Claude Platform on AWS supports two authentication methods: AWS IAM with Signature Version 4 (SigV4) request signing (primary) and API key authentication. Both use the same base URL and request format. ### SigV4 authentication SigV4 is the enterprise-native path and integrates with your existing AWS IAM policies, roles, and auditing. Configure AWS credentials using any method supported by the [AWS default credential provider chain](https://docs.aws.amazon.com/sdkref/latest/guide/standardized-credentials.html): * Environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`) * Shared credentials file (`~/.aws/credentials`) * Shared config file (`~/.aws/config`) including SSO and `credential_process` * Web identity (`AWS_WEB_IDENTITY_TOKEN_FILE` and `AWS_ROLE_ARN`) for IRSA and GitHub Actions * ECS container credentials * EC2 instance metadata service (IMDS) Verify that your credentials are working: ```bash CLI aws sts get-caller-identity ``` ### API key authentication For simpler integration paths (local development and scripts), you can authenticate with an API key instead of SigV4. Set the `ANTHROPIC_AWS_API_KEY` environment variable or pass `apiKey` to the SDK constructor. Generate API keys in the **AWS Console** under **Claude Platform on AWS → API keys**. Choose **Generate a key**, then copy the key value. Grant the `aws-external-anthropic:CallWithBearerToken` IAM action to the principals that should be allowed to use API key authentication. API keys for Claude Platform on AWS are managed in the AWS Console, not the Claude Console. Keys created in the standard [Claude Console](https://platform.claude.com/) (for first-party API access) don't work with the Claude Platform on AWS endpoint. #### Short-term API keys For workloads that need to hand a credential to a separate process (such as an LLM gateway, a serverless function, or a tool that supports bearer-token authentication but not SigV4), generate a short-term API key from your AWS credentials instead of provisioning a long-lived key in the AWS Console. AWS publishes token-generator libraries for [JavaScript](https://github.com/aws/token-generator-for-aws-external-anthropic-js), [Python](https://github.com/aws/token-generator-for-aws-external-anthropic-python), and [Java](https://github.com/aws/token-generator-for-aws-external-anthropic-java). Each library reads your AWS credentials through the standard provider chain and returns a time-limited token that works with the `x-api-key` header. Token lifetime defaults to 12 hours and is capped at the lesser of your requested duration, your AWS credentials' expiry, and 12 hours. See the linked repository READMEs for installation and full configuration options. Pass the generated token to the SDK the same way you'd pass an AWS Console-generated API key: ```python Python from token_generator_for_aws_external_anthropic import TokenGenerator from anthropic import AnthropicAWS token = TokenGenerator(region="us-west-2").get_token() client = AnthropicAWS(api_key=token, aws_region="us-west-2") ``` ```typescript TypeScript import { getTokenProvider } from "@aws/token-generator-for-aws-external-anthropic"; import AnthropicAws from "@anthropic-ai/aws-sdk"; const tokenProvider = getTokenProvider({ region: "us-west-2" }); const token = await tokenProvider(); const client = new AnthropicAws({ apiKey: token, awsRegion: "us-west-2" }); ``` ```java Java import software.amazon.awsexternalanthropic.TokenGenerator; import software.amazon.awssdk.regions.Region; import com.anthropic.aws.backends.AwsBackend; import com.anthropic.client.AnthropicClient; import com.anthropic.client.okhttp.AnthropicOkHttpClient; void main() { String token = TokenGenerator.builder().region(Region.US_WEST_2).build().getToken(); AnthropicClient client = AnthropicOkHttpClient.builder() .backend(AwsBackend.builder() .apiKey(token) .region(Region.US_WEST_2) .workspaceId(System.getenv("ANTHROPIC_AWS_WORKSPACE_ID")) .build()) .build(); } ``` If you can generate the token locally, your process already has SigV4 credentials, and SigV4 authentication is usually the simpler choice. Use short-term keys when the process making API calls is separate from the process that holds AWS credentials. The SDK does not refresh short-term keys automatically. When a token expires, generate a new one and construct a new client. The principal that uses the token still needs the `aws-external-anthropic:CallWithBearerToken` IAM action. ### Credential precedence The platform-specific client resolves authentication in the following order. Argument names vary by language convention: TypeScript and PHP use camelCase as shown, Python and Ruby use snake\_case, Go uses PascalCase with capitalized acronyms, and C# and Java use the language's property or builder idioms. 1. `apiKey` constructor argument → `x-api-key` header 2. `awsAccessKey` + `awsSecretAccessKey` constructor arguments → AWS SigV4 3. `awsProfile` constructor argument → AWS SigV4 with named profile 4. `ANTHROPIC_AWS_API_KEY` environment variable → `x-api-key` header 5. Default AWS credential provider chain → AWS SigV4 ### Region resolution The client reads `AWS_REGION` from the environment if `aws_region`/`awsRegion` is not passed to the constructor, falling back to `AWS_DEFAULT_REGION` for compatibility with the standard AWS SDKs. Region is required. There is no fallback default. Unlike `AnthropicBedrock`, which falls back to `us-east-1`, the `AnthropicAWS`/`AnthropicAws` client raises an error if neither the constructor argument nor the environment variable is set. ## Install an SDK Anthropic's [client SDKs](https://platform.claude.com/docs/en/cli-sdks-libraries/overview) support Claude Platform on AWS. Each SDK provides a platform-specific client class that handles SigV4 signing, region-based base URL construction, and the `anthropic-workspace-id` header. ```bash pip install -U "anthropic[aws]" ``` On macOS with Homebrew Python or other externally managed Python environments, `pip install` can fail with a PEP 668 `externally-managed-environment` error. Create and activate a virtual environment first: `python3 -m venv .venv && source .venv/bin/activate`. ```bash npm install @anthropic-ai/aws-sdk ``` ```bash dotnet add package Anthropic.Aws ``` ```bash go get github.com/anthropics/anthropic-sdk-go ``` ```kotlin Gradle implementation("com.anthropic:anthropic-java-aws:2.53.0") ``` ```xml Maven com.anthropic anthropic-java-aws 2.53.0 ``` ```bash composer require anthropic-ai/sdk aws/aws-sdk-php ``` ```bash gem install anthropic aws-sdk-core ``` SDK clients for Claude Platform on AWS are in beta. ## Available models The following models are available on Claude Platform on AWS: | Model | Model ID | | ----------------- | ----------------- | | Claude Fable 5 | claude-fable-5 | | Claude Opus 4.8 | claude-opus-4-8 | | Claude Opus 4.7 | claude-opus-4-7 | | Claude Opus 4.6 | claude-opus-4-6 | | Claude Sonnet 5 | claude-sonnet-5 | | Claude Sonnet 4.6 | claude-sonnet-4-6 | | Claude Opus 4.5 | claude-opus-4-5 | | Claude Sonnet 4.5 | claude-sonnet-4-5 | | Claude Haiku 4.5 | claude-haiku-4-5 | Model IDs are identical to the first-party Claude API. There are no Bedrock-style ARNs or `anthropic.` prefixes. New models typically launch on Claude Platform on AWS the same day as the first-party Claude API. Upgrading to a newer Claude model? In Claude Code, run `/claude-api migrate` to apply model ID swaps and breaking parameter changes across your codebase. The skill detects which cloud platform your code targets and adjusts model ID formats and feature changes for that platform. See [Migrating to a newer Claude model](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/claude-api-skill#migrating-to-a-newer-claude-model). ## Making requests Claude Platform on AWS uses the same API endpoints as the first-party Claude API. The differences are the base URL, the authentication method, and a required `anthropic-workspace-id` header that identifies which [workspace](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#workspaces) the request targets. Before running these examples, complete the steps in [Before making API calls](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#before-making-api-calls). ```bash cURL # Replace us-west-2 with your AWS region in both the URL and --aws-sigv4 # Omit the x-amz-security-token header if you use long-term IAM user credentials curl "https://aws-external-anthropic.us-west-2.api.aws/v1/messages" \ --aws-sigv4 "aws:amz:us-west-2:aws-external-anthropic" \ --user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" \ -H "x-amz-security-token: $AWS_SESSION_TOKEN" \ -H "content-type: application/json" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-workspace-id: $ANTHROPIC_AWS_WORKSPACE_ID" \ -d '{ "model": "claude-sonnet-5", "max_tokens": 1024, "messages": [ {"role": "user", "content": "Hello!"} ] }' ``` ```bash CLI # Replace us-west-2 with your AWS region # ant reads ANTHROPIC_API_KEY and sends it as x-api-key. Generate a key in the # AWS Console (see API key authentication). export ANTHROPIC_API_KEY="YOUR_AWS_API_KEY" ant messages create \ --base-url https://aws-external-anthropic.us-west-2.api.aws \ --workspace-id "$ANTHROPIC_AWS_WORKSPACE_ID" \ --model claude-sonnet-5 \ --max-tokens 1024 \ --message '{role: user, content: "Hello!"}' \ --transform content ``` ```python Python from anthropic import AnthropicAWS client = AnthropicAWS() message = client.messages.create( model="claude-sonnet-5", max_tokens=1024, messages=[{"role": "user", "content": "Hello!"}], ) print(message) ``` ```typescript TypeScript import AnthropicAws from "@anthropic-ai/aws-sdk"; const client = new AnthropicAws(); const message = await client.messages.create({ model: "claude-sonnet-5", max_tokens: 1024, messages: [{ role: "user", content: "Hello!" }] }); console.log(message); ``` ```csharp C# using Anthropic; using Anthropic.Aws; var client = new AnthropicAwsClient(); var message = await client.Messages.Create(new() { Model = Model.ClaudeSonnet5, MaxTokens = 1024, Messages = [new() { Role = Role.User, Content = "Hello!" }] }); Console.WriteLine(message); ``` ```go Go client, err := anthropicaws.NewClient(context.Background(), anthropicaws.ClientConfig{}) if err != nil { panic(err) } message, err := client.Messages.New(context.Background(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeSonnet5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Hello!")), }, }) if err != nil { panic(err) } fmt.Println(message) ``` ```java Java import com.anthropic.aws.backends.AwsBackend; import com.anthropic.client.AnthropicClient; import com.anthropic.client.okhttp.AnthropicOkHttpClient; import com.anthropic.models.messages.Message; import com.anthropic.models.messages.MessageCreateParams; import com.anthropic.models.messages.Model; void main() { AnthropicClient client = AnthropicOkHttpClient.builder() .backend(AwsBackend.fromEnv()) .build(); Message message = client.messages().create( MessageCreateParams.builder() .model(Model.CLAUDE_SONNET_5) .maxTokens(1024) .addUserMessage("Hello!") .build() ); IO.println(message); } ``` ```php PHP use Anthropic\Aws\Client; $client = new Client(); $message = $client->messages->create( model: 'claude-sonnet-5', maxTokens: 1024, messages: [['role' => 'user', 'content' => 'Hello!']], ); echo $message; ``` ```ruby Ruby require "anthropic" client = Anthropic::AWSClient.new message = client.messages.create( model: "claude-sonnet-5", max_tokens: 1024, messages: [{ role: "user", content: "Hello!" }] ) puts message ``` The client reads `AWS_REGION` (or `AWS_DEFAULT_REGION`) and `ANTHROPIC_AWS_WORKSPACE_ID` from the environment. You can override either by passing `aws_region` / `awsRegion` or `workspace_id` / `workspaceId` to the constructor. Both region and workspace ID are required. The constructor raises an error if either cannot be resolved. The `x-amz-security-token` header (cURL) is only required for temporary credentials such as IAM roles, SSO, or STS. Omit it when using long-term IAM user credentials. The SDK clients handle this automatically based on the credential source. The `--aws-sigv4` value follows the format `aws:amz::`. The SigV4 service name is `aws-external-anthropic`, and the region must match the region in your endpoint URL. A mismatch in either produces a generic signature-rejection error rather than a specific diagnostic. ### Context window Context-window sizes on Claude Platform on AWS are identical to the first-party Claude API. See [Context windows](https://platform.claude.com/docs/en/build-with-claude/context-windows) for per-model limits. ## Feature support Claude Platform on AWS uses Claude API endpoints directly, which means you get full feature parity with the first-party Claude API (except where noted in the [feature limitations](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#features-not-supported)): * **Feature access:** Because Anthropic operates both platforms, most new features and beta headers become available on Claude Platform on AWS without a separate integration step. See [feature limitations](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#features-not-supported) for exceptions. * **Beta features:** Pass the standard `anthropic-beta` header to access beta features, just as you would with the Claude API. * **Agent Skills:** Use pre-built and custom [Agent Skills](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview) with the same `container.skills` parameter and beta headers as the Claude API. All pre-built Skills (PowerPoint, Excel, Word, PDF) work out of the box. * **Code execution:** Run code in Anthropic's managed sandbox using the [code execution tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool). * **Tool use:** Computer use and all other [tool use capabilities](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview) are available. * **Extended thinking:** Enable extended thinking with the same parameters as the Claude API. * **Streaming:** Full SSE streaming support for real-time responses. * **Batch processing:** Submit batch requests for high-throughput workloads. * **Prompt caching:** Cache tools, system prompts, and message history to reduce latency and cost. All prompt caching capabilities (5-minute TTL, 1-hour TTL, and automatic caching) are available. * **Files API:** Upload and reference files across requests. * **Customer-managed encryption keys (CMEK):** [CMEK](https://platform.claude.com/docs/en/manage-claude/cmek) is available with [AWS KMS](https://platform.claude.com/docs/en/manage-claude/cmek-aws-kms) keys only. Google Cloud KMS and Azure Key Vault keys cannot be registered. Create, validate, and attach keys in the [Claude Console](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#using-the-claude-console). The `external_keys` Admin API endpoints are not currently available. The key must be in the same AWS region as the workspace it is attached to. * **Compliance API:** The [Compliance API](https://platform.claude.com/docs/en/manage-claude/compliance-api) is available. Access is authorized through the AWS IAM [`ListComplianceActivities` action](https://platform.claude.com/docs/en/api/claude-platform-on-aws-iam-actions#compliance). See the [comparison table](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#claude-platform-on-aws-vs-amazon-bedrock) for feature-availability differences from Amazon Bedrock. ### Claude Managed Agents [Claude Managed Agents](https://platform.claude.com/docs/en/managed-agents/overview) is available on Claude Platform on AWS, including [agents](https://platform.claude.com/docs/en/managed-agents/agent-setup), [environments](https://platform.claude.com/docs/en/managed-agents/environments), [sessions](https://platform.claude.com/docs/en/managed-agents/sessions), [credential vaults](https://platform.claude.com/docs/en/managed-agents/vaults), [memory stores](https://platform.claude.com/docs/en/managed-agents/memory), [webhooks](https://platform.claude.com/docs/en/managed-agents/webhooks), [multiagent orchestration](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration), and [self-hosted sandboxes](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes). Session behavior on Claude Platform on AWS differs from first-party Claude Managed Agents in one way: * **Autonomous-session reauthentication:** A session can run autonomously, without any [user events](https://platform.claude.com/docs/en/managed-agents/reference#event-types), for up to 6 hours. After 6 hours, the session requires reauthentication before it continues. To reauthenticate, send any user-role event to the session (see [Events and streaming](https://platform.claude.com/docs/en/managed-agents/events-and-streaming)). First-party Claude Managed Agents has no autonomous-session runtime limit. ### Features not supported The following capabilities are not currently available on Claude Platform on AWS: * **HIPAA readiness:** Anthropic's HIPAA-ready program is not available. See [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention). - **Admin API:** Workspace endpoints (create, get, list, update, and archive on `/v1/organizations/workspaces`) are available. Other Admin API endpoints (organization members, workspace members, invites, API keys, usage reports, cost reports, rate limit reports, and external keys) are not currently available. Manage [CMEK](https://platform.claude.com/docs/en/manage-claude/cmek) keys in the Claude Console instead. View usage and cost data in the [Claude Console](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#using-the-claude-console) instead. AWS IAM manages organization membership. - **Workspace member management:** Adding or removing users from individual workspaces is not available. AWS IAM policies on workspace ARNs control access. - **Claude Code workspace and Analytics API:** The Claude Code workspace with automatic rate limits is not available. Claude Code usage appears in the general usage view rather than a dedicated screen. - **OAuth authentication:** Not supported. Use SigV4 or API key authentication. - **Fast mode:** Not available on Claude Platform on AWS. - **OpenAI-compatible API endpoints:** Not available on Claude Platform on AWS. - **MCP tunnels:** Only MCP servers exposed over the public internet are supported. ## Data residency Claude Platform on AWS supports the following inference geographies: * **US:** Inference stays within US data centers. A 1.1x pricing multiplier applies. * **Global:** Inference can route to any Anthropic-operated data center worldwide. Standard pricing applies. The AWS region your workspace is bound to controls which gateway endpoint you call and where AWS-side resources (IAM, CloudTrail, billing) are scoped. It does not pin where model inference runs. To pin inference to a specific geography, set `inference_geo` on each request or configure a workspace default. Set the inference geography per request with the `inference_geo` parameter: The `inference_geo` parameter is supported on Claude 4.6 and later models. Requests with `inference_geo` on Claude Opus 4.5, Claude Sonnet 4.5, or Claude Haiku 4.5 return a 400 error. See [Data residency](https://platform.claude.com/docs/en/manage-claude/data-residency) for model availability details. ```bash cURL # Replace us-west-2 with your AWS region in both the URL and --aws-sigv4 # Omit the x-amz-security-token header if you use long-term IAM user credentials curl "https://aws-external-anthropic.us-west-2.api.aws/v1/messages" \ --aws-sigv4 "aws:amz:us-west-2:aws-external-anthropic" \ --user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" \ -H "x-amz-security-token: $AWS_SESSION_TOKEN" \ -H "content-type: application/json" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-workspace-id: $ANTHROPIC_AWS_WORKSPACE_ID" \ -d '{ "model": "claude-sonnet-5", "max_tokens": 1024, "inference_geo": "us", "messages": [ {"role": "user", "content": "Hello!"} ] }' ``` ```bash CLI # Replace us-west-2 with your AWS region # ant reads ANTHROPIC_API_KEY and sends it as x-api-key. Generate a key in the # AWS Console (see API key authentication). export ANTHROPIC_API_KEY="YOUR_AWS_API_KEY" ant messages create \ --base-url https://aws-external-anthropic.us-west-2.api.aws \ --workspace-id "$ANTHROPIC_AWS_WORKSPACE_ID" \ --model claude-sonnet-5 \ --max-tokens 1024 \ --inference-geo us \ --message '{role: user, content: "Hello!"}' \ --transform content ``` ```python Python from anthropic import AnthropicAWS client = AnthropicAWS() message = client.messages.create( model="claude-sonnet-5", max_tokens=1024, inference_geo="us", messages=[{"role": "user", "content": "Hello!"}], ) print(message) ``` ```typescript TypeScript import AnthropicAws from "@anthropic-ai/aws-sdk"; const client = new AnthropicAws(); const message = await client.messages.create({ model: "claude-sonnet-5", max_tokens: 1024, inference_geo: "us", messages: [{ role: "user", content: "Hello!" }] }); console.log(message); ``` ```csharp C# using Anthropic; using Anthropic.Aws; var client = new AnthropicAwsClient(); var message = await client.Messages.Create(new() { Model = Model.ClaudeSonnet5, MaxTokens = 1024, InferenceGeo = "us", Messages = [new() { Role = Role.User, Content = "Hello!" }] }); Console.WriteLine(message); ``` ```go Go client, err := anthropicaws.NewClient(context.Background(), anthropicaws.ClientConfig{}) if err != nil { panic(err) } message, err := client.Messages.New(context.Background(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeSonnet5, MaxTokens: 1024, InferenceGeo: anthropic.String("us"), Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Hello!")), }, }) if err != nil { panic(err) } fmt.Println(message) ``` ```java Java import com.anthropic.aws.backends.AwsBackend; import com.anthropic.client.AnthropicClient; import com.anthropic.client.okhttp.AnthropicOkHttpClient; import com.anthropic.models.messages.Message; import com.anthropic.models.messages.MessageCreateParams; import com.anthropic.models.messages.Model; void main() { AnthropicClient client = AnthropicOkHttpClient.builder() .backend(AwsBackend.fromEnv()) .build(); Message message = client.messages().create( MessageCreateParams.builder() .model(Model.CLAUDE_SONNET_5) .maxTokens(1024) .inferenceGeo("us") .addUserMessage("Hello!") .build() ); IO.println(message); } ``` ```php PHP use Anthropic\Aws\Client; $client = new Client(); $message = $client->messages->create( model: 'claude-sonnet-5', maxTokens: 1024, inferenceGeo: 'us', messages: [['role' => 'user', 'content' => 'Hello!']], ); echo $message; ``` ```ruby Ruby require "anthropic" client = Anthropic::AWSClient.new message = client.messages.create( model: "claude-sonnet-5", max_tokens: 1024, inference_geo: "us", messages: [{ role: "user", content: "Hello!" }] ) puts message ``` If you omit `inference_geo`, the request uses the workspace's `default_inference_geo` if one is configured, otherwise `global`. Workspace-level inference geography controls (`allowed_inference_geos` and `default_inference_geo`) are also available on Claude Platform on AWS. See [Workspace-level restrictions](https://platform.claude.com/docs/en/manage-claude/data-residency#workspace-level-restrictions). ## Workspaces Inference and resource requests on Claude Platform on AWS target a workspace. You pass the workspace's ID in the `anthropic-workspace-id` header on these API calls. Workspace IDs use the tagged format `wrkspc_` followed by an alphanumeric identifier (for example, `wrkspc_01AbCdEf23GhIj`). See [Obtain your workspace ID](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#obtain-your-workspace-id) if you don't have it yet. ### Workspace scoping Workspaces are bound to a single AWS region. A workspace created in `us-west-2` can only be accessed through the `us-west-2` endpoint. Usage, quotas, cost, files, batches, and Skills all roll up per workspace, giving you per-region breakdowns in the Claude Console. Workspaces also serve as the primary IAM resource for Claude Platform on AWS. You grant or deny access to specific workspaces through AWS IAM policies using the workspace ARN. The ARN's resource segment is the same `wrkspc_`-prefixed ID you pass in the `anthropic-workspace-id` header: ```text wrap arn:aws:aws-external-anthropic:{region}:{account-id}:workspace/{workspace-id} ``` For example: ```text wrap arn:aws:aws-external-anthropic:us-west-2:123456789012:workspace/wrkspc_01AbCdEf23GhIj ``` See [IAM policies](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#iam-policies) for policy examples. ### Managing workspaces Create additional workspaces, rename a workspace, or archive a workspace from the AWS Console **Workspaces** page or with the [Admin API](https://platform.claude.com/docs/en/manage-claude/admin-api) workspace endpoints. A new workspace is bound to the AWS region of the endpoint you call to create it (see [Workspace scoping](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#workspace-scoping)). The Claude Console Workspaces page is read-only. ## Using the Claude Console Claude Platform on AWS uses the standard Claude Console at [platform.claude.com](https://platform.claude.com). When you sign in from the AWS Console, an **Account managed by AWS** indicator appears in the bottom-left of the Claude Console sidebar and the Console scopes to your Claude Platform on AWS organization. It provides usage analytics, cost breakdowns, rate limit visibility, workspace visibility, and pages for managing files, Agent Skills, batch jobs, and Claude Managed Agents resources (agents, sessions, environments, credential vaults, memory stores, and webhooks). ### Signing in Access to the Claude Console is federated through AWS IAM. See [Set up your account](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#set-up-your-account) for the full first-time sign-in flow. In short: 1. Assume an IAM role with the `aws-external-anthropic:AssumeConsole` permission. See [IAM actions for Claude Platform on AWS](https://platform.claude.com/docs/en/api/claude-platform-on-aws-iam-actions#console-access). 2. Navigate to the Claude Platform on AWS page in the [AWS Console](https://console.aws.amazon.com/). 3. Choose **Open Claude Console**. The AWS Console issues a JWT and redirects you to `platform.claude.com`. 4. On first sign-in, you're prompted for an email address. Enter your work email. The platform provisions your Claude Console user just-in-time. Two Claude Console roles are available: **Admin** and **Developer**. The Admin role grants access to all Claude Console pages and settings available for Claude Platform on AWS. The Developer role grants read access to usage, cost, rate limit, and workspace information. Contact your Anthropic account representative to assign the Admin or Developer role to a principal. ### Available pages The **Through AWS gateway** column indicates whether the page reads and writes data through the AWS gateway (and is therefore governed by [IAM actions](https://platform.claude.com/docs/en/api/claude-platform-on-aws-iam-actions)). Pages marked **No** read organization-level metadata directly from Anthropic and bypass IAM action checks. | Page | Available | Through AWS gateway | Notes | | --------------------- | ------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Usage** | Yes | No | View token usage by model, workspace, and dimension. Data can take a few minutes to appear after a request. | | **Cost** | Yes | No | View cost breakdowns by model and workspace. AWS Cost Explorer shows the aggregated [Claude Consumption Unit (CCU)](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#billing) line item. | | **Rate limits** | Yes | No | View rate limits (read-only). Tier increases go through your Anthropic account representative; see [Rate limits and quotas](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#rate-limits-and-quotas). | | **Workspaces** | Yes | No | View per-region workspaces (read-only). | | **Files** | Yes | Yes | View and manage uploaded files. | | **Skills** | Yes | Yes | View and manage Agent Skills. | | **Batches** | Yes | Yes | View and manage batch processing jobs. | | **Agents** | Yes | Yes | View and manage agent definitions. | | **Sessions** | Yes | Yes | View agent sessions and event history. | | **Environments** | Yes | Yes | View and manage cloud sandbox configurations for sessions. | | **Credential vaults** | Yes | Yes | View and manage credential vaults for session authentication. | | **Memory stores** | Yes | Yes | View and manage persistent agent memory. | | **Webhooks** | Yes | Yes | View and manage webhook endpoints under **Settings → Webhooks**. | | **API keys** | No | N/A | Manage API keys in the AWS Console (**Claude Platform on AWS → API keys**). See [API key authentication](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#api-key-authentication). | | **Members** | No | N/A | Not applicable. AWS IAM manages access. | | **Billing** | Yes (limited) | No | Set an organization monthly spend limit; see [Spend limits](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#spend-limits). AWS Marketplace manages invoicing. View cost breakdowns on the Cost page. | | **Claude Code** | No | N/A | View Claude Code usage on the Usage page. | ### Switching organizations The Claude Console does not support organization switching for Claude Platform on AWS. To access a different organization, sign out and reauthenticate through the AWS Console using the IAM role for that organization's AWS account. ## Rate limits and quotas Organizations on Claude Platform on AWS are placed on the Start tier. Anthropic manages rate limits directly, not through AWS quota systems. Organizations on Claude Platform on AWS do not move between usage tiers automatically. Usage-based tier advancement applies to first-party Claude API organizations, not to organizations billed through AWS Marketplace. The self-service **Request rate limit increase** flow in the Claude Console is also not available: the Rate limits page directs you to your Anthropic account representative instead. To request higher limits, contact your Anthropic account representative or [Anthropic support](https://support.claude.com). Include the following in your request: * The models you need raised * Peak input tokens per minute and output tokens per minute for each model (not daily totals) * The approximate share of your input that is cached or repeated context (cache reads don't count toward input-token limits for most models; see [cache-aware ITPM](https://platform.claude.com/docs/en/api/rate-limits#cache-aware-itpm)) Usage tiers are fixed steps: each tier pairs rate limits with a [monthly spend cap](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#spend-limits), and moving to a higher tier raises both. For tier details and per-model limits, see [Rate limits](https://platform.claude.com/docs/en/api/rate-limits). ## Billing Claude Platform on AWS bills through [AWS Marketplace](https://aws.amazon.com/marketplace). Usage is denominated in Claude Consumption Units (CCUs), metered hourly, and invoiced monthly in arrears on your AWS bill. CCUs are not prepaid credits. There is no CCU balance or commitment. For the CCU price, conversion mechanics, discount application, and per-model token rates, see [Claude Platform on AWS pricing](https://platform.claude.com/docs/en/about-claude/pricing#claude-platform-on-aws-pricing). ### Spend limits The Start, Build, and Scale usage tiers each carry a monthly spend cap; see [the per-tier spend caps](https://platform.claude.com/docs/en/api/rate-limits#spend-limits) for current values. The spend cap and rate limits belong to the same tier, so to raise the cap, request a tier increase through your Anthropic account representative or [support](https://support.claude.com) (see [Rate limits and quotas](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#rate-limits-and-quotas)). You can also set your own monthly spend limit to cap what your organization spends: * **Organization spend limit:** Go to [Settings > Billing](https://platform.claude.com/settings/billing) in the [Claude Console](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#using-the-claude-console) to set a monthly spend limit. * **Workspace spend limits:** Set monthly spend limits for individual workspaces from each workspace's **Spend limits** settings. The spend limits you set are soft limits: spend is calculated at list prices and can take about two hours to reflect recent usage. ## Monitoring and logging AWS CloudTrail can capture all requests to Claude Platform on AWS. Workspace, compliance, vault, and webhook operations are logged as Management events by default. Inference, batch, file, skill, model, user profile, and Claude Managed Agents operations (other than vaults and webhooks) are classified as Data events and require explicit data event logging configuration, which incurs additional CloudTrail charges. See the [IAM actions reference](https://platform.claude.com/docs/en/api/claude-platform-on-aws-iam-actions#route-to-action-mapping) for the full event type classification and the [AWS CloudTrail documentation](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/) for configuration details. ### Request IDs Each response includes two request IDs in the response headers: * **AWS request ID (`x-amzn-requestid`):** The primary ID, indexed in CloudTrail. Use this when investigating requests through AWS tooling or when contacting AWS support. * **Anthropic request ID (`request-id`):** The secondary ID. Use this when contacting Anthropic support. ```bash cURL # Replace us-west-2 with your AWS region in both the URL and --aws-sigv4 # -i includes the response headers in the output # Omit the x-amz-security-token header if you use long-term IAM user credentials curl -i "https://aws-external-anthropic.us-west-2.api.aws/v1/messages" \ --aws-sigv4 "aws:amz:us-west-2:aws-external-anthropic" \ --user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" \ -H "x-amz-security-token: $AWS_SESSION_TOKEN" \ -H "content-type: application/json" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-workspace-id: $ANTHROPIC_AWS_WORKSPACE_ID" \ -d '{ "model": "claude-sonnet-5", "max_tokens": 1024, "messages": [ {"role": "user", "content": "Hello!"} ] }' ``` ```bash CLI # The ant CLI's output formats print the response body, not response headers. # To read x-amzn-requestid, use the cURL example (-i) or an SDK example. ``` ```python Python from anthropic import AnthropicAWS client = AnthropicAWS() response = client.messages.with_raw_response.create( model="claude-sonnet-5", max_tokens=1024, messages=[{"role": "user", "content": "Hello!"}], ) print(response.headers.get("x-amzn-requestid")) # AWS request ID print(response.headers.get("request-id")) # Anthropic request ID message = response.parse() print(message.content) ``` ```typescript TypeScript import AnthropicAws from "@anthropic-ai/aws-sdk"; const client = new AnthropicAws(); const { data: message, response } = await client.messages .create({ model: "claude-sonnet-5", max_tokens: 1024, messages: [{ role: "user", content: "Hello!" }] }) .withResponse(); console.log(response.headers.get("x-amzn-requestid")); // AWS request ID console.log(response.headers.get("request-id")); // Anthropic request ID console.log(message.content); ``` ```csharp C# using Anthropic; using Anthropic.Aws; var client = new AnthropicAwsClient(); var response = await client.WithRawResponse.Messages.Create(new() { Model = Model.ClaudeSonnet5, MaxTokens = 1024, Messages = [new() { Role = Role.User, Content = "Hello!" }] }); Console.WriteLine(response.Headers.GetValues("x-amzn-requestid").First()); // AWS request ID Console.WriteLine(response.Headers.GetValues("request-id").First()); // Anthropic request ID Console.WriteLine(response.Value.Content); ``` ```go Go client, err := anthropicaws.NewClient(context.Background(), anthropicaws.ClientConfig{}) if err != nil { panic(err) } var response *http.Response message, err := client.Messages.New( context.Background(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeSonnet5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Hello!")), }, }, option.WithResponseInto(&response), ) if err != nil { panic(err) } fmt.Println(response.Header.Get("x-amzn-requestid")) // AWS request ID fmt.Println(response.Header.Get("request-id")) // Anthropic request ID fmt.Println(message.Content) ``` ```java Java import com.anthropic.aws.backends.AwsBackend; import com.anthropic.client.AnthropicClient; import com.anthropic.client.okhttp.AnthropicOkHttpClient; import com.anthropic.core.http.HttpResponseFor; import com.anthropic.models.messages.Message; import com.anthropic.models.messages.MessageCreateParams; import com.anthropic.models.messages.Model; void main() { AnthropicClient client = AnthropicOkHttpClient.builder() .backend(AwsBackend.fromEnv()) .build(); HttpResponseFor response = client.messages().withRawResponse().create( MessageCreateParams.builder() .model(Model.CLAUDE_SONNET_5) .maxTokens(1024) .addUserMessage("Hello!") .build() ); IO.println(response.headers().values("x-amzn-requestid").get(0)); // AWS request ID IO.println(response.requestId().orElse(null)); // Anthropic request ID IO.println(response.parse().content()); } ``` ```php PHP use Anthropic\Aws\Client; $client = new Client(); $response = $client->messages->raw->create( model: 'claude-sonnet-5', maxTokens: 1024, messages: [['role' => 'user', 'content' => 'Hello!']], ); echo $response->getHeaderLine('x-amzn-requestid') . "\n"; // AWS request ID echo $response->getHeaderLine('request-id') . "\n"; // Anthropic request ID echo $response->parse()->content; ``` ```ruby Ruby # Accessing raw response headers is not currently supported in the Ruby SDK. # To inspect the x-amzn-requestid header, use one of the other SDK examples. ``` Anthropic recommends logging your activity on at least a 30-day rolling basis to understand usage patterns and investigate issues. AWS CloudTrail is configured within your AWS account. Enabling logging does not provide AWS or Anthropic access to your content beyond what is necessary for billing and service operation. ## Migrating from Amazon Bedrock If you currently use Claude on Bedrock, migrating to Claude Platform on AWS requires changes throughout your integration. SigV4 signing remains supported, but the signing context, base URL, API format, model IDs, SDK client and package, streaming format, request headers, and region availability all change. Claude Platform on AWS also provisions a new Anthropic organization. The following table summarizes the differences. ### What changes The migration delta depends on which Bedrock integration you're coming from. The following table shows both the [current Bedrock integration](https://platform.claude.com/docs/en/build-with-claude/claude-in-amazon-bedrock) (Messages API at `bedrock-mantle.{region}.api.aws`) and the [legacy InvokeModel integration](https://platform.claude.com/docs/en/build-with-claude/claude-on-amazon-bedrock-legacy). | Aspect | From [Claude in Amazon Bedrock](https://platform.claude.com/docs/en/build-with-claude/claude-in-amazon-bedrock) | From [Amazon Bedrock (Opus 4.6 and earlier)](https://platform.claude.com/docs/en/build-with-claude/claude-on-amazon-bedrock-legacy) | To Claude Platform on AWS | | -------------------------- | --------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Base URL** | `bedrock-mantle.{region}.api.aws` | `bedrock-runtime.{region}.amazonaws.com` | `aws-external-anthropic.{region}.api.aws` | | **API format** | Messages API at `/anthropic/v1/messages` | Bedrock Converse / InvokeModel | Claude API (`/v1/{endpoint}`) | | **Model IDs** | anthropic.claude-haiku-4-5 | anthropic.claude-haiku-4-5-20251001-v1:0(with a `us.` or `global.` inference profile prefix) | claude-haiku-4-5 | | **SDK client** | `AnthropicBedrockMantle` | `AnthropicBedrock` / Bedrock SDK | Platform-specific client (see [Install an SDK](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#install-an-sdk)), in beta | | **SDK package** | `anthropic[bedrock]`, `@anthropic-ai/bedrock-sdk`, and others | `anthropic[bedrock]`, `@anthropic-ai/bedrock-sdk`, or AWS SDK | `anthropic[aws]`, `@anthropic-ai/aws-sdk`, and others (see [Install an SDK](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#install-an-sdk)) | | **SigV4 service name** | `bedrock-mantle` | `bedrock` | `aws-external-anthropic` | | **Streaming format** | SSE | AWS EventStream | SSE (same as Claude API) | | **Workspace header** | Not applicable | Not applicable | `anthropic-workspace-id` required | | **Region availability** | See [Amazon Bedrock regions](https://docs.aws.amazon.com/bedrock/latest/userguide/bedrock-regions.html) | See [Amazon Bedrock regions](https://docs.aws.amazon.com/bedrock/latest/userguide/bedrock-regions.html) | All AWS commercial regions | | **Anthropic organization** | None required | None required | New organization created at sign-up. Existing organizations can't be converted (see [Moving from an existing Anthropic organization](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#moving-from-an-existing-anthropic-organization)) | If you're on the current Bedrock integration, the request body format is already the Messages API. The changes are the base URL, SigV4 service name, model IDs, and adding the `anthropic-workspace-id` header. If you're on the legacy InvokeModel or Converse API, you'll also rewrite the request and response shapes to the Messages API format. See [Claude on Amazon Bedrock (Opus 4.6 and earlier)](https://platform.claude.com/docs/en/build-with-claude/claude-on-amazon-bedrock-legacy) for the request-shape mapping. ### What you gain * Typically same-day access to new models and features (see [feature limitations](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#features-not-supported)) * Agent Skills for document generation (PowerPoint, Excel, Word, PDF) * Code execution in Anthropic's managed sandbox * Beta features through the `anthropic-beta` header (see [feature limitations](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#features-not-supported)) * Claude Console for quota visibility and usage analytics * Direct Anthropic support * API key authentication as an alternative to SigV4 (see [API key authentication](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#api-key-authentication)) ### What stays the same * AWS IAM authentication (SigV4) * AWS as the invoicing party. The billing channel changes from native AWS service to AWS Marketplace (see [Commercial considerations](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#commercial-considerations)). * AWS commitment retirement ### Migration pitfalls **Enable outbound web identity federation first.** If your AWS account has not previously used Claude Platform on AWS, you must [enable outbound web identity federation](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#enable-outbound-web-identity-federation) once per account before making requests. Without this step, all requests fail with a federation error (see [Enable outbound web identity federation](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#enable-outbound-web-identity-federation) for the exact error and remediation). This step is not required for Bedrock. **Zero Data Retention (ZDR) is opt-in on Claude Platform on AWS.** On Bedrock, AWS is the data processor and Anthropic does not retain inference inputs or outputs. Anthropic's ZDR program does not apply there. On Claude Platform on AWS, Anthropic processes inference data as an independent data processor, and ZDR follows the first-party Claude API model: it is available on request through your Anthropic account representative. Confirm ZDR enrollment before migrating production workloads that depend on data-retention guarantees. ### Commercial considerations * **Anthropic terms of service:** Using Claude Platform on AWS requires accepting Anthropic's Commercial Terms of Service and Usage Policy. If your organization hasn't already accepted these (for example, if you've only used Claude through Bedrock), you're prompted during account setup. See [Set up your account](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#set-up-your-account). * **Discounts and private offers:** Negotiated discounts and AWS Marketplace private offers don't transfer automatically between Bedrock and Claude Platform on AWS. Work with your Anthropic account representative to set up commercial terms for Claude Platform on AWS. ## IAM policies Claude Platform on AWS integrates with AWS IAM for access control. You grant or deny access to specific API actions on specific workspaces using standard IAM policy syntax. The SigV4 service name and IAM action namespace is `aws-external-anthropic`. Actions follow the pattern `aws-external-anthropic:` (for example, `aws-external-anthropic:CreateInference`). ### Example: deny batch inference The following policy allows real-time inference while blocking batch processing: ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "aws-external-anthropic:CreateInference", "aws-external-anthropic:CountTokens", "aws-external-anthropic:GetModel", "aws-external-anthropic:ListModels", "aws-external-anthropic:GetWorkspace" ], "Resource": "arn:aws:aws-external-anthropic:*:*:workspace/*" }, { "Effect": "Allow", "Action": "aws-external-anthropic:ListWorkspaces", "Resource": "*" }, { "Effect": "Deny", "Action": [ "aws-external-anthropic:CreateBatchInference", "aws-external-anthropic:GetBatchInference", "aws-external-anthropic:ListBatchInferences" ], "Resource": "*" } ] } ``` The `GetBatchInference` action authorizes both the batch metadata route and the batch results route. Denying it blocks both reads. For a Deny-only policy suitable for ZDR-sensitive workloads, see [Feature lockdown for a ZDR-sensitive workspace](https://platform.claude.com/docs/en/api/claude-platform-on-aws-iam-actions#feature-lockdown-for-a-zdr-sensitive-workspace). `ListWorkspaces` is account-scoped, so it appears in a separate Allow statement with `"Resource": "*"`. Specifying a workspace ARN on an account-scoped action has no effect (see [Provisioning automation](https://platform.claude.com/docs/en/api/claude-platform-on-aws-iam-actions#provisioning-automation)). This policy assumes AWS SigV4 authentication. If the principal authenticates with an API key, also add `aws-external-anthropic:CallWithBearerToken` to the `"Resource": "*"` Allow statement. `CallWithBearerToken` is a route-less authentication-layer action that does not bind to a workspace ARN. See [Per-customer workspace isolation](https://platform.claude.com/docs/en/api/claude-platform-on-aws-iam-actions#per-customer-workspace-isolation) for the two-statement pattern. ### Managed policies AWS provides five managed policies (`AnthropicFullAccess`, `AnthropicReadOnlyAccess`, `AnthropicInferenceAccess`, `AnthropicLimitedAccess`, and `AnthropicSelfHostedEnvironmentAccess`) for common access patterns. For the actions each policy grants, the complete list of IAM actions, the route-to-action mapping, and additional policy examples, see [IAM actions for Claude Platform on AWS](https://platform.claude.com/docs/en/api/claude-platform-on-aws-iam-actions#managed-policies). ## Next steps Explore Claude's advanced features and capabilities. Learn about Claude Platform on AWS pricing and Claude Consumption Unit rates. As safer and more capable models launch, Anthropic regularly retires older ones. See all API deprecations, along with recommended replacements. ## Additional resources View usage, cost, and workspaces in the Claude Console. Sign in through the AWS Console. Use AWS-operated Claude if you need AWS as the sole data processor. Manage your AWS Marketplace subscription and billing. ## Managed Agents ### First steps --- title: Claude Managed Agents overview url: https://platform.claude.com/docs/en/managed-agents/overview description: Pre-built, configurable agent harness that runs in managed infrastructure. Best for long-running tasks and asynchronous work. --- Anthropic offers two ways to build with Claude, each suited to different use cases: | | Messages API | Claude Managed Agents | | -------------- | ------------------------------------------- | ------------------------------------------------------------------------- | | **What it is** | Direct model prompting access | Pre-built, configurable agent harness that runs in managed infrastructure | | **Best for** | Custom agent loops and fine-grained control | Long-running tasks and asynchronous work | Claude Managed Agents provides the harness and infrastructure for running Claude as an autonomous agent. Instead of building your own agent loop, tool execution, and runtime, you get a fully managed environment where Claude can read files, run commands, browse the web, and run code securely. The harness supports built-in prompt caching, compaction, and other performance optimizations for high-quality, efficient agent outputs. To build your own agent loop with direct model access instead, see [Using the Messages API](https://platform.claude.com/docs/en/build-with-claude/working-with-messages). Claude Managed Agents is also available on Claude Platform on AWS, with some differences in feature availability and session behavior. See [Claude Managed Agents](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#claude-managed-agents) in the Claude Platform on AWS guide. Create your first agent session Create a session and send your first event Event types, rate limits, CLI flags, and other lookup tables ## Core concepts Claude Managed Agents is built around four concepts: | Concept | Description | | --------------- | ----------------------------------------------------------------------------------------------------------------------------- | | **Agent** | The model, system prompt, tools, MCP servers, and skills | | **Environment** | Configuration for where sessions run: an Anthropic-managed cloud sandbox, or a self-hosted sandbox on your own infrastructure | | **Session** | A running agent instance within an environment, performing a specific task and generating outputs | | **Events** | Messages exchanged between your application and the agent (user turns, tool results, status updates) | ## How it works Define the model, system prompt, tools, MCP servers, and skills. Create the agent once and reference it by ID across sessions. Configure where the agent runs: a cloud sandbox, or a [self-hosted sandbox](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes) on your own infrastructure. Launch a session that references your agent and environment configuration. Send user messages as events. Claude autonomously runs tools and streams back results through server-sent events (SSE). Event history is persisted server-side and can be fetched in full. Send additional user events to guide the agent mid-execution, or interrupt it to change direction. ## When to use Claude Managed Agents Claude Managed Agents is best for workloads that need: * **Long-running execution:** Tasks that run for minutes or hours with multiple tool calls * **Cloud infrastructure:** Secure sandboxes with pre-installed packages and network access * **Self-hosted execution:** Sandboxes on infrastructure you control for compliance or data-residency requirements * **Minimal infrastructure:** No need to build your own agent loop, sandbox, or tool execution layer * **Stateful sessions:** Persistent filesystems and conversation history across multiple interactions * **Scheduled execution:** Recurring agent runs on a cron schedule through [scheduled deployments](https://platform.claude.com/docs/en/managed-agents/scheduled-deployments) ## Supported tools Claude Managed Agents gives Claude access to a set of built-in tools: * **Bash:** Run shell commands in the sandbox * **File operations:** Read, write, edit, glob, and grep files in the sandbox * **Web search and fetch:** Search the web and retrieve content from URLs * **MCP servers:** Connect to external tool providers See [Tools](https://platform.claude.com/docs/en/managed-agents/tools) for the full list and configuration options. ## Beta access Claude Managed Agents is in beta. All Managed Agents endpoints require the `managed-agents-2026-04-01` beta header. The SDK sets the beta header automatically. Behaviors may be refined between releases to improve outputs. To get started, you need: 1. A [Claude API key](https://platform.claude.com/settings/keys) 2. The `managed-agents-2026-04-01` beta header on all requests 3. Access to Claude Managed Agents (enabled by default for all API accounts) Within the beta, [MCP tunnels](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/overview) and [dreaming](https://platform.claude.com/docs/en/managed-agents/dreams) are in a more limited research preview. [Request access](https://claude.com/form/claude-managed-agents) to enable them. Claude Managed Agents is stateful by design: sessions are long-running, resume cleanly after pauses, and store conversation history, sandbox state, and outputs server-side. Because of this, Managed Agents is not currently eligible for [Zero Data Retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#zero-data-retention-zdr-scope) or HIPAA Business Associate Agreement (BAA) coverage. You retain control over this data: you can [delete sessions](https://platform.claude.com/docs/en/managed-agents/session-operations#deleting-a-session), and separately delete any [files](https://platform.claude.com/docs/en/build-with-claude/files#delete-a-file) you uploaded, at any time through the API. For eligibility across all features, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#feature-eligibility). See [Rate limits](https://platform.claude.com/docs/en/managed-agents/reference#rate-limits) and [Branding guidelines](https://platform.claude.com/docs/en/managed-agents/reference#branding-guidelines) in the reference. --- title: Get started with Claude Managed Agents url: https://platform.claude.com/docs/en/managed-agents/quickstart description: Create your first autonomous agent. --- This guide walks you through creating an agent, setting up an environment, starting a session, and streaming agent responses. **Prefer an interactive walkthrough?** Run `/claude-api managed-agents-onboard` in the latest version of [Claude Code](https://claude.com/product/claude-code) for a guided setup and interactive question-answering. ## Core concepts | Concept | Description | | --------------- | ----------------------------------------------------------------------------------------------------------------------------- | | **Agent** | The model, system prompt, tools, MCP servers, and skills | | **Environment** | Configuration for where sessions run: an Anthropic-managed cloud sandbox, or a self-hosted sandbox on your own infrastructure | | **Session** | A running agent instance within an environment, performing a specific task and generating outputs | | **Events** | Messages exchanged between your application and the agent (user turns, tool results, status updates) | ## Prerequisites * A [Claude Console account](https://platform.claude.com) * An [API key](https://platform.claude.com/settings/keys) ## Install the CLI ```bash brew install anthropics/tap/ant ``` For Linux environments, download the release binary directly. ```bash VERSION=1.22.1 OS=$(uname -s | tr '[:upper:]' '[:lower:]') case $(uname -m) in x86_64) ARCH=amd64 ;; aarch64) ARCH=arm64 ;; esac curl -fsSL "https://github.com/anthropics/anthropic-cli/releases/download/v${VERSION}/ant_${VERSION}_${OS}_${ARCH}.tar.gz" \ | sudo tar -xz -C /usr/local/bin ant ``` You can find all releases on the [GitHub releases page](https://github.com/anthropics/anthropic-cli/releases). You can also install the CLI from source using `go install`. Requires Go 1.25 or later. ```bash go install github.com/anthropics/anthropic-cli/cmd/ant@latest ``` The binary is placed in `$(go env GOPATH)/bin`. Add it to your `PATH` if it isn't already: ```bash export PATH="$PATH:$(go env GOPATH)/bin" ``` Check the installation: ```bash ant --version ``` ## Install the SDK ```bash pip install anthropic ``` ```bash npm install @anthropic-ai/sdk ``` ```groovy Gradle implementation("com.anthropic:anthropic-java:2.53.0") ``` ```bash go get github.com/anthropics/anthropic-sdk-go ``` ```bash dotnet add package Anthropic ``` ```bash bundle add anthropic ``` ```bash composer require "anthropic-ai/sdk" "guzzlehttp/guzzle:^7" ``` Set your API key as an environment variable: ```bash export ANTHROPIC_API_KEY="your-api-key-here" ``` ## Create your first session Managed Agents API requests require the `managed-agents-2026-04-01` beta header, except memory store endpoints, which use `agent-memory-2026-07-22` instead. The SDK sets the correct beta header automatically. See [Beta headers](https://platform.claude.com/docs/en/api/beta-headers#endpoint-specific-headers). Create an agent that defines the model, system prompt, and available tools. ```bash cURL set -euo pipefail agent=$( curl -sS --fail-with-body https://api.anthropic.com/v1/agents \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d @- <<'EOF' { "name": "Coding Assistant", "model": "claude-opus-5", "system": "You are a helpful coding assistant. Write clean, well-documented code.", "tools": [ {"type": "agent_toolset_20260401"} ] } EOF ) AGENT_ID=$(jq -er '.id' <<<"$agent") AGENT_VERSION=$(jq -er '.version' <<<"$agent") echo "Agent ID: $AGENT_ID, version: $AGENT_VERSION" ``` ```bash CLI AGENT_ID=$(ant beta:agents create \ --name "Coding Assistant" \ --model '{id: claude-opus-5}' \ --system "You are a helpful coding assistant. Write clean, well-documented code." \ --tool '{type: agent_toolset_20260401}' \ --transform id --raw-output) echo "Agent ID: $AGENT_ID" ``` ```python Python from anthropic import Anthropic client = Anthropic() agent = client.beta.agents.create( name="Coding Assistant", model="claude-opus-5", system="You are a helpful coding assistant. Write clean, well-documented code.", tools=[ {"type": "agent_toolset_20260401"}, ], ) print(f"Agent ID: {agent.id}, version: {agent.version}") ``` ```typescript TypeScript import Anthropic from "@anthropic-ai/sdk"; const client = new Anthropic(); const agent = await client.beta.agents.create({ name: "Coding Assistant", model: "claude-opus-5", system: "You are a helpful coding assistant. Write clean, well-documented code.", tools: [ { type: "agent_toolset_20260401" }, ], }); console.log(`Agent ID: ${agent.id}, version: ${agent.version}`); ``` ```csharp C# using Anthropic; using Anthropic.Models.Beta.Agents; using Anthropic.Models.Beta.Environments; using Anthropic.Models.Beta.Sessions; using Anthropic.Models.Beta.Sessions.Events; var client = new AnthropicClient(); var agent = await client.Beta.Agents.Create(new() { Name = "Coding Assistant", Model = BetaManagedAgentsModel.ClaudeOpus5, System = "You are a helpful coding assistant. Write clean, well-documented code.", Tools = [ new BetaManagedAgentsAgentToolset20260401Params { Type = "agent_toolset_20260401", }, ], }); Console.WriteLine($"Agent ID: {agent.ID}, version: {agent.Version}"); ``` ```go Go package main import ( "context" "fmt" "github.com/anthropics/anthropic-sdk-go" ) func main() { client := anthropic.NewClient() ctx := context.Background() agent, err := client.Beta.Agents.New(ctx, anthropic.BetaAgentNewParams{ Name: "Coding Assistant", Model: anthropic.BetaManagedAgentsModelConfigParams{ ID: anthropic.BetaManagedAgentsModelClaudeOpus5, }, System: anthropic.String("You are a helpful coding assistant. Write clean, well-documented code."), Tools: []anthropic.BetaAgentNewParamsToolUnion{{ OfAgentToolset20260401: &anthropic.BetaManagedAgentsAgentToolset20260401Params{ Type: anthropic.BetaManagedAgentsAgentToolset20260401ParamsTypeAgentToolset20260401, }, }}, }) if err != nil { panic(err) } fmt.Printf("Agent ID: %s, version: %d\n", agent.ID, agent.Version) ``` ```java Java import com.anthropic.client.okhttp.AnthropicOkHttpClient; import com.anthropic.models.beta.agents.AgentCreateParams; import com.anthropic.models.beta.agents.BetaManagedAgentsAgentToolset20260401Params; import com.anthropic.models.beta.agents.BetaManagedAgentsModel; import com.anthropic.models.beta.environments.BetaCloudConfigParams; import com.anthropic.models.beta.environments.BetaUnrestrictedNetwork; import com.anthropic.models.beta.environments.EnvironmentCreateParams; import com.anthropic.models.beta.sessions.SessionCreateParams; import com.anthropic.models.beta.sessions.events.BetaManagedAgentsStreamSessionEvents; import com.anthropic.models.beta.sessions.events.BetaManagedAgentsUserMessageEventParams; import com.anthropic.models.beta.sessions.events.EventSendParams; void main() { var client = AnthropicOkHttpClient.fromEnv(); var agent = client.beta().agents().create(AgentCreateParams.builder() .name("Coding Assistant") .model(BetaManagedAgentsModel.CLAUDE_OPUS_5) .system("You are a helpful coding assistant. Write clean, well-documented code.") .addTool(BetaManagedAgentsAgentToolset20260401Params.builder() .type(BetaManagedAgentsAgentToolset20260401Params.Type.AGENT_TOOLSET_20260401) .build()) .build()); IO.println("Agent ID: " + agent.id() + ", version: " + agent.version()); ``` ```php PHP use Anthropic\Client; $client = new Client(); $agent = $client->beta->agents->create( name: 'Coding Assistant', model: 'claude-opus-5', system: 'You are a helpful coding assistant. Write clean, well-documented code.', tools: [ ['type' => 'agent_toolset_20260401'], ], ); echo "Agent ID: {$agent->id}, version: {$agent->version}\n"; ``` ```ruby Ruby require "anthropic" client = Anthropic::Client.new agent = client.beta.agents.create( name: "Coding Assistant", model: "claude-opus-5", system_: "You are a helpful coding assistant. Write clean, well-documented code.", tools: [{type: "agent_toolset_20260401"}] ) puts "Agent ID: #{agent.id}, version: #{agent.version}" ``` The `agent_toolset_20260401` tool type enables the full set of pre-built agent tools (bash, file operations, web search, and more). See [Tools](https://platform.claude.com/docs/en/managed-agents/tools) for the complete list and per-tool configuration options. Save the returned `agent.id`. You'll reference it in every session you create. An environment defines the sandbox where your agent runs. ```bash cURL environment=$( curl -sS --fail-with-body https://api.anthropic.com/v1/environments \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d @- <<'EOF' { "name": "quickstart-env", "config": { "type": "cloud", "networking": {"type": "unrestricted"} } } EOF ) ENVIRONMENT_ID=$(jq -er '.id' <<<"$environment") echo "Environment ID: $ENVIRONMENT_ID" ``` ```bash CLI ENVIRONMENT_ID=$(ant beta:environments create \ --name "quickstart-env" \ --config '{type: cloud, networking: {type: unrestricted}}' \ --transform id --raw-output) echo "Environment ID: $ENVIRONMENT_ID" ``` ```python Python environment = client.beta.environments.create( name="quickstart-env", config={ "type": "cloud", "networking": {"type": "unrestricted"}, }, ) print(f"Environment ID: {environment.id}") ``` ```typescript TypeScript const environment = await client.beta.environments.create({ name: "quickstart-env", config: { type: "cloud", networking: { type: "unrestricted" }, }, }); console.log(`Environment ID: ${environment.id}`); ``` ```csharp C# var environment = await client.Beta.Environments.Create(new() { Name = "quickstart-env", Config = new BetaCloudConfigParams { Networking = new BetaUnrestrictedNetwork() }, }); Console.WriteLine($"Environment ID: {environment.ID}"); ``` ```go Go environment, err := client.Beta.Environments.New(ctx, anthropic.BetaEnvironmentNewParams{ Name: "quickstart-env", Config: anthropic.BetaEnvironmentNewParamsConfigUnion{ OfCloud: &anthropic.BetaCloudConfigParams{ Networking: anthropic.BetaCloudConfigParamsNetworkingUnion{ OfUnrestricted: &anthropic.BetaUnrestrictedNetworkParam{}, }, }, }, }) if err != nil { panic(err) } fmt.Printf("Environment ID: %s\n", environment.ID) ``` ```java Java var environment = client.beta().environments().create(EnvironmentCreateParams.builder() .name("quickstart-env") .config(BetaCloudConfigParams.builder() .networking(BetaUnrestrictedNetwork.builder().build()) .build()) .build()); IO.println("Environment ID: " + environment.id()); ``` ```php PHP $environment = $client->beta->environments->create( name: 'quickstart-env', config: ['type' => 'cloud', 'networking' => ['type' => 'unrestricted']], ); echo "Environment ID: {$environment->id}\n"; ``` ```ruby Ruby environment = client.beta.environments.create( name: "quickstart-env", config: {type: "cloud", networking: {type: "unrestricted"}} ) puts "Environment ID: #{environment.id}" ``` Save the returned `environment.id`. You'll reference it in every session you create. To run the sandbox on your own infrastructure instead of a cloud sandbox, see [Self-hosted sandboxes](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes) . Create a session that references your agent and environment. ```bash cURL session=$( curl -sS --fail-with-body https://api.anthropic.com/v1/sessions \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d @- <beta->sessions->create( agent: $agent->id, environmentID: $environment->id, title: 'Quickstart session', ); echo "Session ID: {$session->id}\n"; ``` ```ruby Ruby session = client.beta.sessions.create( agent: agent.id, environment_id: environment.id, title: "Quickstart session" ) puts "Session ID: #{session.id}" ``` Open a stream, send a user event, then process events as they arrive: ```bash cURL # This workflow does not translate well to a one-off shell command. # Use one of the SDK examples in this code group instead. ``` ```bash CLI # This workflow does not translate well to a one-off shell command. # Use one of the SDK examples in this code group instead. ``` ```python Python with client.beta.sessions.events.stream(session.id) as stream: # Send the user message after the stream opens client.beta.sessions.events.send( session.id, events=[ { "type": "user.message", "content": [ { "type": "text", "text": "Create a Python script that generates the first 20 Fibonacci numbers and saves them to fibonacci.txt", }, ], }, ], ) # Process streaming events for event in stream: match event.type: case "agent.message": for block in event.content: print(block.text, end="") case "agent.tool_use": print(f"\n[Using tool: {event.name}]") case "session.status_idle": print("\n\nAgent finished.") break ``` ```typescript TypeScript const stream = await client.beta.sessions.events.stream(session.id); // Send the user message after the stream opens await client.beta.sessions.events.send(session.id, { events: [ { type: "user.message", content: [ { type: "text", text: "Create a Python script that generates the first 20 Fibonacci numbers and saves them to fibonacci.txt", }, ], }, ], }); // Process streaming events for await (const event of stream) { if (event.type === "agent.message") { for (const block of event.content) { process.stdout.write(block.text); } } else if (event.type === "agent.tool_use") { console.log(`\n[Using tool: ${event.name}]`); } else if (event.type === "session.status_idle") { console.log("\n\nAgent finished."); break; } } ``` ```csharp C# var stream = client.Beta.Sessions.Events.StreamStreaming(session.ID); // Send the user message after the stream opens await client.Beta.Sessions.Events.Send(session.ID, new() { Events = [ new BetaManagedAgentsUserMessageEventParams { Type = "user.message", Content = [ new BetaManagedAgentsTextBlock { Type = "text", Text = "Create a Python script that generates the first 20 Fibonacci numbers and saves them to fibonacci.txt", }, ], }, ], }); // Process streaming events await foreach (var ev in stream) { if (ev.Value is BetaManagedAgentsAgentMessageEvent message) { foreach (var block in message.Content) { Console.Write(block.Text); } } else if (ev.Value is BetaManagedAgentsAgentToolUseEvent toolUse) { Console.WriteLine($"\n[Using tool: {toolUse.Name}]"); } else if (ev.Value is BetaManagedAgentsSessionStatusIdleEvent) { Console.WriteLine("\n\nAgent finished."); break; } } ``` ```go Go stream := client.Beta.Sessions.Events.StreamEvents(ctx, session.ID, anthropic.BetaSessionEventStreamParams{}) defer stream.Close() // Send the user message after the stream opens _, err = client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{ Events: []anthropic.BetaManagedAgentsEventParamsUnion{{ OfUserMessage: &anthropic.BetaManagedAgentsUserMessageEventParams{ Type: anthropic.BetaManagedAgentsUserMessageEventParamsTypeUserMessage, Content: []anthropic.BetaManagedAgentsUserMessageEventParamsContentUnion{{ OfText: &anthropic.BetaManagedAgentsTextBlockParam{ Type: anthropic.BetaManagedAgentsTextBlockTypeText, Text: "Create a Python script that generates the first 20 Fibonacci numbers and saves them to fibonacci.txt", }, }}, }, }}, }) if err != nil { panic(err) } // Process streaming events loop: for stream.Next() { switch event := stream.Current().AsAny().(type) { case anthropic.BetaManagedAgentsAgentMessageEvent: for _, block := range event.Content { fmt.Print(block.Text) } case anthropic.BetaManagedAgentsAgentToolUseEvent: fmt.Printf("\n[Using tool: %s]\n", event.Name) case anthropic.BetaManagedAgentsSessionStatusIdleEvent: fmt.Print("\n\nAgent finished.\n") break loop } } if err := stream.Err(); err != nil { panic(err) } ``` ```java Java try (var stream = client.beta().sessions().events().streamStreaming(session.id())) { // Send the user message after the stream opens client.beta().sessions().events().send(session.id(), EventSendParams.builder() .addEvent(BetaManagedAgentsUserMessageEventParams.builder() .type(BetaManagedAgentsUserMessageEventParams.Type.USER_MESSAGE) .addTextContent("Create a Python script that generates the first 20 Fibonacci numbers and saves them to fibonacci.txt") .build()) .build()); // Process streaming events for (var event : (Iterable) stream.stream()::iterator) { if (event.isAgentMessage()) { event.asAgentMessage().content().forEach(block -> block.text().ifPresent(textBlock -> IO.print(textBlock.text()))); } else if (event.isAgentToolUse()) { IO.println("\n[Using tool: " + event.asAgentToolUse().name() + "]"); } else if (event.isSessionStatusIdle()) { IO.println("\n\nAgent finished."); break; } } } ``` ```php PHP $stream = $client->beta->sessions->events->streamStream($session->id); // Send the user message after the stream opens $client->beta->sessions->events->send( $session->id, events: [ [ 'type' => 'user.message', 'content' => [ ['type' => 'text', 'text' => 'Create a Python script that generates the first 20 Fibonacci numbers and saves them to fibonacci.txt'], ], ], ], ); // Process streaming events foreach ($stream as $event) { match ($event->type) { 'agent.message' => print(implode('', array_map(fn($block) => $block->text, $event->content))), 'agent.tool_use' => print("\n[Using tool: {$event->name}]\n"), 'session.status_idle' => print("\n\nAgent finished.\n"), default => null, }; if ($event->type === 'session.status_idle') { break; } } ``` ```ruby Ruby stream = client.beta.sessions.events.stream_events(session.id) # Send the user message after the stream opens client.beta.sessions.events.send_( session.id, events: [{ type: "user.message", content: [{type: "text", text: "Create a Python script that generates the first 20 Fibonacci numbers and saves them to fibonacci.txt"}] }] ) # Process streaming events stream.each do |event| case event.type in :"agent.message" event.content.each { print it.text } in :"agent.tool_use" puts "\n[Using tool: #{event.name}]" in :"session.status_idle" puts "\n\nAgent finished." break else # ignore other event types end end ``` The agent writes a Python script, runs it in the sandbox, and verifies the output file was created. Your output looks similar to this: ```text wrap I'll create a Python script that generates the first 20 Fibonacci numbers and saves them to a file. [Using tool: write] [Using tool: bash] The script ran successfully. Let me verify the output file. [Using tool: bash] fibonacci.txt contains the first 20 Fibonacci numbers (0 through 4181). Agent finished. ``` ## What's happening When you send a user event, Claude Managed Agents: 1. **Provisions a sandbox:** Your environment configuration determines how it's built. 2. **Runs the agent loop:** Claude determines which tools to use based on your message. 3. **Runs tools:** File writes, bash commands, and other tool calls run inside the sandbox. 4. **Streams events:** You receive real-time updates as the agent works. 5. **Goes idle:** The agent emits a `session.status_idle` event when it has nothing more to do. ## Build a complete app Each of these quickstarts pairs Claude Managed Agents with a popular chat framework to make a complete, runnable application. In each one, the framework renders the chat surface while a managed session runs the agent loop server-side: the session holds the transcript, runs tools in a sandbox, and streams events that the front end renders. A research analyst in a browser chat built with Vercel's Chat SDK. Each conversation is one persistent session that streams its reply while a live feed shows the tool calls. Swapping the Chat SDK adapter moves the same handler to Slack, Teams, Discord, or WhatsApp. A spreadsheet analyst in a chat built from assistant-ui primitives. Sessions are the thread list, one reducer turns the session event log into messages and tool cards, and each bash command renders an inline Allow/Deny gate before it runs. A personal finance assistant in a CopilotKit chat. The AG-UI adapter for Claude Managed Agents maps each chat thread to a managed session and streams replies token by token, and custom tools render interactive charts inline in the conversation. ## Next steps Create reusable, versioned agent configurations Customize networking and sandbox settings Enable specific tools for your agent Handle events and steer the agent mid-execution Run your agent on a recurring cron schedule Distill a document corpus once into a knowledge wiki, then answer repeated questions from it at a fraction of the cost --- title: Build in Console url: https://platform.claude.com/docs/en/managed-agents/onboarding description: Create, test, and iterate on agents visually in Console, then run them from your code with the API. --- [Console](https://platform.claude.com/workspaces/default/agent-quickstart/) provides a visual interface for creating and configuring agents. It lets you iterate on configuration interactively before writing code. Managed Agents API requests require the `managed-agents-2026-04-01` beta header, except memory store endpoints, which use `agent-memory-2026-07-22` instead. The SDK sets the correct beta header automatically. See [Beta headers](https://platform.claude.com/docs/en/api/beta-headers#endpoint-specific-headers). ## How to build an agent The [visual interface](https://platform.claude.com/workspaces/default/agent-quickstart/) walks you through each field of an agent definition: * **Model and system prompt:** Pick a model and write the system prompt in a full-width editor. * **MCP servers:** Add remote MCP servers by URL and authenticate your agent to take action on your behalf. * **Tools:** Extend your agent's capabilities using a pre-built agent toolset and MCP tools. * **Skills:** Attach Anthropic or custom skills from your organization's library. As you configure, Console shows the equivalent API request so you can copy it into your code once you're satisfied. ## Testing an agent Console includes an inline session runner. After configuring your agent, you can start a test session directly, send messages, and watch the event stream without leaving the page. This is the fastest way to check that your system prompt and tool selection produce the behavior you expect. ## From Console to your codebase Once your agent works as expected: 1. Copy the agent ID and [environment ID](https://platform.claude.com/docs/en/managed-agents/environments) from Console. 2. Reference them in your code when [creating sessions](https://platform.claude.com/docs/en/managed-agents/sessions): ```bash cURL session=$(curl -fsSL https://api.anthropic.com/v1/sessions \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d '{ "agent": "agent_01J8XkN5uT3vHpLqRfWdY2", "environment_id": "env_01K2mPsT7hNwR4jXuLvCqD8", "title": "My first session" }') ``` ```bash CLI ant beta:sessions create \ --agent agent_01J8XkN5uT3vHpLqRfWdY2 \ --environment-id env_01K2mPsT7hNwR4jXuLvCqD8 \ --title "My first session" ``` ```python Python session = client.beta.sessions.create( agent="agent_01J8XkN5uT3vHpLqRfWdY2", environment_id="env_01K2mPsT7hNwR4jXuLvCqD8", title="My first session", ) ``` ```typescript TypeScript const session = await client.beta.sessions.create({ agent: "agent_01J8XkN5uT3vHpLqRfWdY2", environment_id: "env_01K2mPsT7hNwR4jXuLvCqD8", title: "My first session" }); ``` ```csharp C# var session = await client.Beta.Sessions.Create(new() { Agent = "agent_01J8XkN5uT3vHpLqRfWdY2", EnvironmentID = "env_01K2mPsT7hNwR4jXuLvCqD8", Title = "My first session", }); ``` ```go Go session, err := client.Beta.Sessions.New(ctx, anthropic.BetaSessionNewParams{ Agent: anthropic.BetaSessionNewParamsAgentUnion{ OfString: anthropic.String("agent_01J8XkN5uT3vHpLqRfWdY2"), }, EnvironmentID: "env_01K2mPsT7hNwR4jXuLvCqD8", Title: anthropic.String("My first session"), }) if err != nil { panic(err) } ``` ```java Java var session = client.beta().sessions().create( SessionCreateParams.builder() .agent("agent_01J8XkN5uT3vHpLqRfWdY2") .environmentId("env_01K2mPsT7hNwR4jXuLvCqD8") .title("My first session") .build() ); ``` ```php PHP $session = $client->beta->sessions->create( agent: 'agent_01J8XkN5uT3vHpLqRfWdY2', environmentID: 'env_01K2mPsT7hNwR4jXuLvCqD8', title: 'My first session', ); ``` ```ruby Ruby session = client.beta.sessions.create( agent: "agent_01J8XkN5uT3vHpLqRfWdY2", environment_id: "env_01K2mPsT7hNwR4jXuLvCqD8", title: "My first session" ) ``` --- title: Migration url: https://platform.claude.com/docs/en/managed-agents/migration description: Move an existing agent built on the Messages API or the Claude Agent SDK to Claude Managed Agents. --- Claude Managed Agents replaces your hand-written agent loop with managed infrastructure. This page covers what changes when you migrate from a custom loop built on the [Messages API](https://platform.claude.com/docs/en/build-with-claude/working-with-messages) or from the [Claude Agent SDK](https://code.claude.com/docs/en/agent-sdk/overview). Managed Agents API requests require the `managed-agents-2026-04-01` beta header, except memory store endpoints, which use `agent-memory-2026-07-22` instead. The SDK sets the correct beta header automatically. See [Beta headers](https://platform.claude.com/docs/en/api/beta-headers#endpoint-specific-headers). ## From a Messages API agent loop If you built an agent by calling `messages.create` in a `while` loop, running tool calls yourself, and appending results to the conversation history, most of that code goes away. ### What you stop managing | Before | After | | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------- | | You maintain the conversation history array and pass it back on every turn. | The session stores history server-side. Send events, receive events. | | You iterate `tool_use` content blocks, run each tool, and loop back with `tool_result` messages. | Pre-built tools run inside the sandbox automatically. You only handle custom tools through `agent.custom_tool_use` events. | | You provision your own sandbox for running agent-generated code. | The session sandbox handles code execution, file operations, and bash. | | You decide when the loop is done. | The session emits `session.status_idle` when the agent has nothing more to do. | ### Code comparison **Before** (Messages API loop, simplified): ```python Python messages = [{"role": "user", "content": task}] while True: response = client.messages.create( model="claude-opus-5", max_tokens=1024, messages=messages, tools=tools, ) messages.append({"role": "assistant", "content": response.content}) if response.stop_reason == "end_turn": break for block in response.content: if block.type == "tool_use": result = execute_tool(block.name, block.input) messages.append( { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": block.id, "content": result, } ], } ) ``` ```typescript TypeScript const messages: Anthropic.MessageParam[] = [{ role: "user", content: task }]; while (true) { const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, messages, tools }); messages.push({ role: "assistant", content: response.content }); if (response.stop_reason === "end_turn") { break; } for (const block of response.content) { if (block.type === "tool_use") { const result = executeTool(block.name, block.input); messages.push({ role: "user", content: [ { type: "tool_result", tool_use_id: block.id, content: result } ] }); } } } ``` ```csharp C# List messages = [new() { Role = Role.User, Content = task }]; while (true) { var response = await client.Messages.Create(new() { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = messages, Tools = tools, }); messages.Add(new() { Role = Role.Assistant, Content = new([.. response.Content.Select(block => new ContentBlockParam(block.Json))]), }); if (response.StopReason == StopReason.EndTurn) { break; } foreach (var block in response.Content) { if (block.Value is ToolUseBlock toolUse) { var result = ExecuteTool(toolUse.Name, toolUse.Input); messages.Add(new() { Role = Role.User, Content = new([new ToolResultBlockParam { ToolUseID = toolUse.ID, Content = result }]), }); } } } ``` ```go Go messages := []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock(task)), } for { response, err := client.Messages.New(ctx, anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: messages, Tools: tools, }) if err != nil { log.Fatal(err) } messages = append(messages, response.ToParam()) if response.StopReason == anthropic.StopReasonEndTurn { break } for _, block := range response.Content { if toolUse, ok := block.AsAny().(anthropic.ToolUseBlock); ok { result := executeTool(toolUse.Name, toolUse.Input) messages = append(messages, anthropic.NewUserMessage( anthropic.NewToolResultBlock(toolUse.ID, result, false), )) } } } ``` ```java Java var messages = new ArrayList(); messages.add(MessageParam.builder() .role(MessageParam.Role.USER) .content(task) .build()); while (true) { var response = client.messages().create(MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .messages(messages) .tools(tools) .build()); messages.add(response.toParam()); if (StopReason.END_TURN.equals(response.stopReason().orElse(null))) { break; } for (var block : response.content()) { block.toolUse().ifPresent(toolUse -> { var result = executeTool(toolUse.name(), toolUse._input()); messages.add(MessageParam.builder() .role(MessageParam.Role.USER) .contentOfBlockParams(List.of( ContentBlockParam.ofToolResult(ToolResultBlockParam.builder() .toolUseId(toolUse.id()) .content(result) .build()))) .build()); }); } } ``` ```php PHP $messages = [['role' => 'user', 'content' => $task]]; while (true) { $response = $client->messages->create( model: 'claude-opus-5', maxTokens: 1024, messages: $messages, tools: $tools, ); $messages[] = ['role' => 'assistant', 'content' => $response->content]; if ($response->stopReason === 'end_turn') { break; } foreach ($response->content as $block) { if ($block->type === 'tool_use') { $result = executeTool($block->name, $block->input); $messages[] = [ 'role' => 'user', 'content' => [ [ 'type' => 'tool_result', 'tool_use_id' => $block->id, 'content' => $result, ], ], ]; } } } ``` ```ruby Ruby messages = [{ role: "user", content: task }] loop do response = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: messages, tools: tools ) messages << { role: "assistant", content: response.content } break if response.stop_reason == :end_turn response.content.each do |block| next unless block.type == :tool_use result = execute_tool(block.name, block.input) messages << { role: "user", content: [ { type: "tool_result", tool_use_id: block.id, content: result } ] } end end ``` **After** (Claude Managed Agents): ```bash cURL agent=$( curl --fail-with-body -sS "https://api.anthropic.com/v1/agents?beta=true" \ -H "x-api-key: ${ANTHROPIC_API_KEY}" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ --json '{ "name": "Task Runner", "model": "claude-opus-5", "tools": [{"type": "agent_toolset_20260401"}] }' ) agent_id=$(jq -r '.id' <<< "${agent}") session_id=$( curl --fail-with-body -sS "https://api.anthropic.com/v1/sessions?beta=true" \ -H "x-api-key: ${ANTHROPIC_API_KEY}" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ --json "$(jq -n --argjson a "${agent}" --arg env "${environment_id}" \ '{agent: {type: "agent", id: $a.id, version: $a.version}, environment_id: $env}')" \ | jq -r '.id' ) # Open the SSE stream in the background, then send the user message. stream_log=$(mktemp) curl --fail-with-body -sS -N \ "https://api.anthropic.com/v1/sessions/${session_id}/events/stream?beta=true" \ -H "x-api-key: ${ANTHROPIC_API_KEY}" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ > "${stream_log}" & stream_pid=$! curl --fail-with-body -sS \ "https://api.anthropic.com/v1/sessions/${session_id}/events?beta=true" \ -H "x-api-key: ${ANTHROPIC_API_KEY}" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ --json "$(jq -n --arg text "${task}" \ '{events: [{type: "user.message", content: [{type: "text", text: $text}]}]}')" \ > /dev/null # Wait for the session to go idle. grep exits at the first match, and # reading via process substitution means the shell doesn't wait for # tail (a foreground `tail -f | grep -m1` pipeline would hang: tail # only dies on its next write, which never comes once the stream is idle). grep -m1 '"session.status_idle"' <(tail -f -n +1 "${stream_log}") > /dev/null kill "${stream_pid}" 2>/dev/null || true ``` ```bash CLI { read -r _ agent_id; read -r _ agent_version; } < <(ant beta:agents create \ --name "Task Runner" \ --model claude-opus-5 \ --tool '{type: agent_toolset_20260401}' \ --transform '{id,version}' --format yaml) session_id=$(ant beta:sessions create \ --agent "{type: agent, id: $agent_id, version: $agent_version}" \ --environment-id "$environment_id" \ --transform id --raw-output) # Open the stream first, then send the user message exec {stream}< <(ant beta:sessions:events stream \ --session-id "$session_id" \ --transform type --raw-output) ant beta:sessions:events send \ --session-id "$session_id" \ --event "{type: user.message, content: [{type: text, text: \"$task\"}]}" \ > /dev/null # Wait for the session to go idle (grep exits at the first match) grep -m1 -x 'session.status_idle' <&"$stream" > /dev/null exec {stream}<&- ``` ```python Python agent = client.beta.agents.create( name="Task Runner", model="claude-opus-5", tools=[{"type": "agent_toolset_20260401"}], ) session = client.beta.sessions.create( agent={"type": "agent", "id": agent.id, "version": agent.version}, environment_id=environment.id, ) with client.beta.sessions.events.stream(session.id) as stream: client.beta.sessions.events.send( session.id, events=[{"type": "user.message", "content": [{"type": "text", "text": task}]}], ) for event in stream: if event.type == "session.status_idle": break ``` ```typescript TypeScript const agent = await client.beta.agents.create({ name: "Task Runner", model: "claude-opus-5", tools: [{ type: "agent_toolset_20260401" }] }); const session = await client.beta.sessions.create({ agent: { type: "agent", id: agent.id, version: agent.version }, environment_id: environment.id }); 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: task }] } ] }); for await (const event of stream) { if (event.type === "session.status_idle") { break; } } ``` ```csharp C# var agent = await client.Beta.Agents.Create(new() { Name = "Task Runner", Model = BetaManagedAgentsModel.ClaudeOpus5, Tools = [ new BetaManagedAgentsAgentToolset20260401Params { Type = "agent_toolset_20260401", }, ], }); var session = await client.Beta.Sessions.Create(new() { Agent = new BetaManagedAgentsAgentParams { Type = "agent", ID = agent.ID, Version = agent.Version, }, EnvironmentID = environment.ID, }); var stream = client.Beta.Sessions.Events.StreamStreaming(session.ID); await client.Beta.Sessions.Events.Send(session.ID, new() { Events = [ new BetaManagedAgentsUserMessageEventParams { Type = "user.message", Content = [new BetaManagedAgentsTextBlock { Type = "text", Text = task }], }, ], }); await foreach (var streamEvent in stream) { if (streamEvent.Value is BetaManagedAgentsSessionStatusIdleEvent) { break; } } ``` ```go Go agent, err := client.Beta.Agents.New(ctx, anthropic.BetaAgentNewParams{ Name: "Task Runner", Model: anthropic.BetaManagedAgentsModelConfigParams{ ID: anthropic.BetaManagedAgentsModelClaudeOpus5, }, Tools: []anthropic.BetaAgentNewParamsToolUnion{{ OfAgentToolset20260401: &anthropic.BetaManagedAgentsAgentToolset20260401Params{ Type: anthropic.BetaManagedAgentsAgentToolset20260401ParamsTypeAgentToolset20260401, }, }}, }) if err != nil { log.Fatal(err) } session, err := client.Beta.Sessions.New(ctx, anthropic.BetaSessionNewParams{ Agent: anthropic.BetaSessionNewParamsAgentUnion{ OfBetaManagedAgentsAgents: &anthropic.BetaManagedAgentsAgentParams{ Type: anthropic.BetaManagedAgentsAgentParamsTypeAgent, ID: agent.ID, Version: anthropic.Int(agent.Version), }, }, EnvironmentID: environment.ID, }) if err != nil { log.Fatal(err) } stream := client.Beta.Sessions.Events.StreamEvents(ctx, session.ID, anthropic.BetaSessionEventStreamParams{}) defer stream.Close() _, err = client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{ Events: []anthropic.BetaManagedAgentsEventParamsUnion{{ OfUserMessage: &anthropic.BetaManagedAgentsUserMessageEventParams{ Type: anthropic.BetaManagedAgentsUserMessageEventParamsTypeUserMessage, Content: []anthropic.BetaManagedAgentsUserMessageEventParamsContentUnion{{ OfText: &anthropic.BetaManagedAgentsTextBlockParam{ Type: anthropic.BetaManagedAgentsTextBlockTypeText, Text: task, }, }}, }, }}, }) if err != nil { log.Fatal(err) } for stream.Next() { event := stream.Current() if event.Type == "session.status_idle" { break } } if err := stream.Err(); err != nil { log.Fatal(err) } ``` ```java Java var agent = client.beta().agents().create( AgentCreateParams.builder() .name("Task Runner") .model(BetaManagedAgentsModel.CLAUDE_OPUS_5) .addTool( BetaManagedAgentsAgentToolset20260401Params.builder() .type(BetaManagedAgentsAgentToolset20260401Params.Type.AGENT_TOOLSET_20260401) .build() ) .build() ); 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() ); 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(task) .build() ) .build() ); stream.stream() .takeWhile(event -> !event.isSessionStatusIdle()) .forEach(_ -> {}); } ``` ```php PHP $agent = $client->beta->agents->create( name: 'Task Runner', model: 'claude-opus-5', tools: [ BetaManagedAgentsAgentToolset20260401Params::with( type: 'agent_toolset_20260401', ), ], ); $session = $client->beta->sessions->create( agent: BetaManagedAgentsAgentParams::with( type: 'agent', id: $agent->id, version: $agent->version, ), environmentID: $environment->id, ); $stream = $client->beta->sessions->events->streamStream($session->id); $client->beta->sessions->events->send( $session->id, events: [ [ 'type' => 'user.message', 'content' => [['type' => 'text', 'text' => $task]], ], ], ); foreach ($stream as $event) { if ($event->type === 'session.status_idle') { break; } } ``` ```ruby Ruby agent = client.beta.agents.create( name: "Task Runner", model: "claude-opus-5", tools: [{type: "agent_toolset_20260401"}] ) session = client.beta.sessions.create( agent: {type: "agent", id: agent.id, version: agent.version}, environment_id: environment.id ) stream = client.beta.sessions.events.stream_events(session.id) client.beta.sessions.events.send_( session.id, events: [{type: "user.message", content: [{type: "text", text: task}]}] ) stream.each do break if it.type == :"session.status_idle" end ``` ### What you still control * **System prompt and model:** Same fields, now on the agent definition. * **Custom tools:** Still declared with JSON Schema. Execution moves from inline handling to responding to `agent.custom_tool_use` events. See [Session event stream](https://platform.claude.com/docs/en/managed-agents/events-and-streaming). * **Context:** You can still inject context through the system prompt, [file resources](https://platform.claude.com/docs/en/managed-agents/files), or [skills](https://platform.claude.com/docs/en/managed-agents/skills). ## From the Claude Agent SDK If you built with the [Claude Agent SDK](https://code.claude.com/docs/en/agent-sdk/overview), you're already working with agents, tools, and sessions as concepts. The difference is where they run: the SDK runs in a process you operate, while Managed Agents runs in Anthropic's infrastructure. Most of the migration is mapping SDK configuration objects to their API-side equivalents. ### What changes | Agent SDK | Managed Agents | | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ClaudeAgentOptions(...)` constructed per run | `client.beta.agents.create(...)` once; the Agent is persisted and versioned server-side. See [Agent setup](https://platform.claude.com/docs/en/managed-agents/agent-setup). | | `async with ClaudeSDKClient(...)` or `query(...)` | `client.beta.sessions.create(...)` then send and receive [events](https://platform.claude.com/docs/en/managed-agents/events-and-streaming). | | `@tool`-decorated functions dispatched automatically by the SDK | Declare as `{"type": "custom", ...}` on the Agent; your client handles `agent.custom_tool_use` events and replies with `user.custom_tool_result`. See [Tools](https://platform.claude.com/docs/en/managed-agents/tools). | | Built-in tools run in your process against your filesystem | `{"type": "agent_toolset_20260401"}` runs the same tools inside the session sandbox against `/workspace`. | | `cwd`, `add_dirs` point at local paths | Upload or mount [files](https://platform.claude.com/docs/en/managed-agents/files) as session resources. | | `system_prompt` and the `CLAUDE.md` hierarchy | A single `system` string on the Agent. Each update that changes the agent produces a new server-side version; pin sessions to a specific version to promote or roll back without a deploy. See [Agent setup](https://platform.claude.com/docs/en/managed-agents/agent-setup). | | `mcp_servers` configured and authenticated in one place | Declare servers on the Agent; provide credentials through a [Vault](https://platform.claude.com/docs/en/managed-agents/vaults) on the Session. | | `permission_mode`, `can_use_tool` | Per-tool [`permission_policy`](https://platform.claude.com/docs/en/managed-agents/permission-policies); send `user.tool_confirmation` events for `always_ask` tools. | ### Code comparison **Before** (Agent SDK): ```python Python from claude_agent_sdk import ( ClaudeAgentOptions, ClaudeSDKClient, create_sdk_mcp_server, tool, ) @tool("get_weather", "Get the current weather for a city.", {"city": str}) async def get_weather(args: dict) -> dict: return {"content": [{"type": "text", "text": f"{args['city']}: 18°C, clear"}]} options = ClaudeAgentOptions( model="claude-opus-5", system_prompt="You are a concise weather assistant.", mcp_servers={ "weather": create_sdk_mcp_server("weather", "1.0", tools=[get_weather]) }, ) async with ClaudeSDKClient(options=options) as agent: await agent.query("What's the weather in Tokyo?") async for msg in agent.receive_response(): print(msg) ``` ```typescript TypeScript import { createSdkMcpServer, query, tool } from "@anthropic-ai/claude-agent-sdk"; import { z } from "zod"; const getWeather = tool( "get_weather", "Get the current weather for a city.", { city: z.string() }, async (args) => ({ content: [{ type: "text", text: `${args.city}: 18°C, clear` }] }) ); for await (const message of query({ prompt: "What's the weather in Tokyo?", options: { model: "claude-opus-5", systemPrompt: "You are a concise weather assistant.", mcpServers: { weather: createSdkMcpServer({ name: "weather", version: "1.0", tools: [getWeather] }) } } })) { console.log(message); } ``` **After** (Managed Agents): ```python Python from anthropic import Anthropic client = Anthropic() agent = client.beta.agents.create( name="weather-agent", model="claude-opus-5", system="You are a concise weather assistant.", tools=[ { "type": "custom", "name": "get_weather", "description": "Get the current weather for a city.", "input_schema": { "type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"], }, } ], ) environment = client.beta.environments.create( name="weather-env", config={"type": "cloud", "networking": {"type": "unrestricted"}}, ) session = client.beta.sessions.create( agent={"type": "agent", "id": agent.id, "version": agent.version}, environment_id=environment.id, ) def get_weather(city: str) -> str: return f"{city}: 18°C, clear" with client.beta.sessions.events.stream(session.id) as stream: client.beta.sessions.events.send( session.id, events=[ { "type": "user.message", "content": [{"type": "text", "text": "What's the weather in Tokyo?"}], } ], ) for event in stream: if event.type == "agent.message": print( "".join(block.text for block in event.content if block.type == "text") ) elif event.type == "agent.custom_tool_use": result = get_weather(**event.input) client.beta.sessions.events.send( session.id, events=[ { "type": "user.custom_tool_result", "custom_tool_use_id": event.id, "content": [{"type": "text", "text": result}], } ], ) elif ( event.type == "session.status_idle" and event.stop_reason and event.stop_reason.type == "end_turn" ): break ``` ```typescript TypeScript import Anthropic from "@anthropic-ai/sdk"; const client = new Anthropic(); const agent = await client.beta.agents.create({ name: "weather-agent", model: "claude-opus-5", system: "You are a concise weather assistant.", tools: [ { type: "custom", name: "get_weather", description: "Get the current weather for a city.", input_schema: { type: "object", properties: { city: { type: "string" } }, required: ["city"] } } ] }); const environment = await client.beta.environments.create({ name: "weather-env", config: { type: "cloud", networking: { type: "unrestricted" } } }); const session = await client.beta.sessions.create({ agent: { type: "agent", id: agent.id, version: agent.version }, environment_id: environment.id }); function getWeather({ city }: Record): string { return `${city}: 18°C, clear`; } 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: "What's the weather in Tokyo?" }] } ] }); for await (const event of stream) { if (event.type === "agent.message") { for (const block of event.content) { if (block.type === "text") { console.log(block.text); } } } else if (event.type === "agent.custom_tool_use") { const result = getWeather(event.input); await client.beta.sessions.events.send(session.id, { events: [ { type: "user.custom_tool_result", custom_tool_use_id: event.id, content: [{ type: "text", text: result }] } ] }); } else if (event.type === "session.status_idle" && event.stop_reason?.type === "end_turn") { break; } } ``` ```csharp C# using System.Text.Json; using Anthropic.Models.Beta.Agents; using Anthropic.Models.Beta.Environments; using Anthropic.Models.Beta.Sessions; using Anthropic.Models.Beta.Sessions.Events; AnthropicClient client = new(); var agent = await client.Beta.Agents.Create(new() { Name = "weather-agent", Model = BetaManagedAgentsModel.ClaudeOpus5, System = "You are a concise weather assistant.", Tools = [ new BetaManagedAgentsCustomToolParams { Type = "custom", Name = "get_weather", Description = "Get the current weather for a city.", InputSchema = new() { Properties = new Dictionary { ["city"] = JsonSerializer.SerializeToElement(new { type = "string" }), }, Required = ["city"], }, }, ], }); var environment = await client.Beta.Environments.Create(new() { Name = "weather-env", Config = new BetaCloudConfigParams { Networking = new BetaUnrestrictedNetwork(), }, }); var session = await client.Beta.Sessions.Create(new() { Agent = new BetaManagedAgentsAgentParams { Type = "agent", ID = agent.ID, Version = agent.Version, }, EnvironmentID = environment.ID, }); static string GetWeather(string city) => $"{city}: 18°C, clear"; using var stream = await client.Beta.Sessions.Events.WithRawResponse.StreamStreaming(session.ID); await client.Beta.Sessions.Events.Send(session.ID, new() { Events = [ new BetaManagedAgentsUserMessageEventParams { Type = "user.message", Content = [new BetaManagedAgentsTextBlock { Type = "text", Text = "What's the weather in Tokyo?" }], }, ], }); await foreach (var streamEvent in stream.Enumerate()) { if (streamEvent.Value is BetaManagedAgentsAgentMessageEvent message) { Console.WriteLine(string.Concat(message.Content.Select(block => block.Text))); } else if (streamEvent.Value is BetaManagedAgentsAgentCustomToolUseEvent toolUse) { var result = GetWeather(toolUse.Input["city"].GetString()!); await client.Beta.Sessions.Events.Send(session.ID, new() { Events = [ new BetaManagedAgentsUserCustomToolResultEventParams { Type = "user.custom_tool_result", CustomToolUseID = toolUse.ID, Content = [ new BetaManagedAgentsTextBlock { Type = "text", Text = result, }, ], }, ], }); } else if (streamEvent.Value is BetaManagedAgentsSessionStatusIdleEvent idle && idle.StopReason?.Value is BetaManagedAgentsSessionEndTurn) { break; } } ``` ```go Go client := anthropic.NewClient() ctx := context.Background() agent, err := client.Beta.Agents.New(ctx, anthropic.BetaAgentNewParams{ Name: "weather-agent", Model: anthropic.BetaManagedAgentsModelConfigParams{ ID: anthropic.BetaManagedAgentsModelClaudeOpus5, }, System: anthropic.String("You are a concise weather assistant."), Tools: []anthropic.BetaAgentNewParamsToolUnion{{ OfCustom: &anthropic.BetaManagedAgentsCustomToolParams{ Type: anthropic.BetaManagedAgentsCustomToolParamsTypeCustom, Name: "get_weather", Description: "Get the current weather for a city.", InputSchema: anthropic.BetaManagedAgentsCustomToolInputSchemaParam{ Properties: map[string]any{ "city": map[string]any{"type": "string"}, }, Required: []string{"city"}, }, }, }}, }) if err != nil { panic(err) } environment, err := client.Beta.Environments.New(ctx, anthropic.BetaEnvironmentNewParams{ Name: "weather-env", Config: anthropic.BetaEnvironmentNewParamsConfigUnion{ OfCloud: &anthropic.BetaCloudConfigParams{ Networking: anthropic.BetaCloudConfigParamsNetworkingUnion{ OfUnrestricted: &anthropic.BetaUnrestrictedNetworkParam{}, }, }, }, }) if err != nil { panic(err) } session, err := client.Beta.Sessions.New(ctx, anthropic.BetaSessionNewParams{ Agent: anthropic.BetaSessionNewParamsAgentUnion{ OfBetaManagedAgentsAgents: &anthropic.BetaManagedAgentsAgentParams{ Type: anthropic.BetaManagedAgentsAgentParamsTypeAgent, ID: agent.ID, Version: anthropic.Int(agent.Version), }, }, EnvironmentID: environment.ID, }) if err != nil { panic(err) } getWeather := func(city string) string { return fmt.Sprintf("%s: 18°C, clear", city) } stream := client.Beta.Sessions.Events.StreamEvents(ctx, session.ID, anthropic.BetaSessionEventStreamParams{}) defer stream.Close() _, err = client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{ Events: []anthropic.BetaManagedAgentsEventParamsUnion{{ OfUserMessage: &anthropic.BetaManagedAgentsUserMessageEventParams{ Type: anthropic.BetaManagedAgentsUserMessageEventParamsTypeUserMessage, Content: []anthropic.BetaManagedAgentsUserMessageEventParamsContentUnion{{ OfText: &anthropic.BetaManagedAgentsTextBlockParam{ Type: anthropic.BetaManagedAgentsTextBlockTypeText, Text: "What's the weather in Tokyo?", }, }}, }, }}, }) if err != nil { panic(err) } loop: for stream.Next() { event := stream.Current() switch event.Type { case "agent.message": for _, block := range event.AsAgentMessage().Content { if block.Type == "text" { fmt.Println(block.Text) } } case "agent.custom_tool_use": toolUse := event.AsAgentCustomToolUse() result := getWeather(toolUse.Input["city"].(string)) if _, err := client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{ Events: []anthropic.BetaManagedAgentsEventParamsUnion{{ OfUserCustomToolResult: &anthropic.BetaManagedAgentsUserCustomToolResultEventParams{ Type: anthropic.BetaManagedAgentsUserCustomToolResultEventParamsTypeUserCustomToolResult, CustomToolUseID: toolUse.ID, Content: []anthropic.BetaManagedAgentsUserCustomToolResultEventParamsContentUnion{{ OfText: &anthropic.BetaManagedAgentsTextBlockParam{ Type: anthropic.BetaManagedAgentsTextBlockTypeText, Text: result, }, }}, }, }}, }); err != nil { panic(err) } case "session.status_idle": idle := event.AsSessionStatusIdle() if _, ok := idle.StopReason.AsAny().(anthropic.BetaManagedAgentsSessionEndTurn); ok { break loop } } } if err := stream.Err(); err != nil { panic(err) } ``` ```java Java import java.util.Map; import java.util.function.Function; import com.anthropic.models.beta.agents.AgentCreateParams; import com.anthropic.models.beta.agents.BetaManagedAgentsCustomToolInputSchema; import com.anthropic.models.beta.agents.BetaManagedAgentsCustomToolParams; import com.anthropic.models.beta.agents.BetaManagedAgentsModel; import com.anthropic.models.beta.environments.BetaCloudConfigParams; import com.anthropic.models.beta.environments.BetaUnrestrictedNetwork; 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.BetaManagedAgentsStreamSessionEvents; import com.anthropic.models.beta.sessions.events.BetaManagedAgentsUserCustomToolResultEventParams; import com.anthropic.models.beta.sessions.events.BetaManagedAgentsUserMessageEventParams; import com.anthropic.models.beta.sessions.events.EventSendParams; var client = AnthropicOkHttpClient.fromEnv(); var agent = client.beta().agents().create(AgentCreateParams.builder() .name("weather-agent") .model(BetaManagedAgentsModel.CLAUDE_OPUS_5) .system("You are a concise weather assistant.") .addTool(BetaManagedAgentsCustomToolParams.builder() .type(BetaManagedAgentsCustomToolParams.Type.CUSTOM) .name("get_weather") .description("Get the current weather for a city.") .inputSchema(BetaManagedAgentsCustomToolInputSchema.builder() .properties(BetaManagedAgentsCustomToolInputSchema.Properties.builder() .putAdditionalProperty("city", JsonValue.from(Map.of("type", "string"))) .build()) .addRequired("city") .build()) .build()) .build()); var environment = client.beta().environments().create(EnvironmentCreateParams.builder() .name("weather-env") .config(BetaCloudConfigParams.builder() .networking(BetaUnrestrictedNetwork.builder().build()) .build()) .build()); 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()); Function getWeather = city -> city + ": 18°C, clear"; 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("What's the weather in Tokyo?") .build()) .build()); for (var event : (Iterable) stream.stream()::iterator) { if (event.isAgentMessage()) { for (var block : event.asAgentMessage().content()) { block.text().ifPresent(textBlock -> IO.println(textBlock.text())); } } else if (event.isAgentCustomToolUse()) { var toolUse = event.asAgentCustomToolUse(); var city = toolUse.input()._additionalProperties().get("city").asStringOrThrow(); var result = getWeather.apply(city); client.beta().sessions().events().send( session.id(), EventSendParams.builder() .addEvent(BetaManagedAgentsUserCustomToolResultEventParams.builder() .type(BetaManagedAgentsUserCustomToolResultEventParams.Type.USER_CUSTOM_TOOL_RESULT) .customToolUseId(toolUse.id()) .addTextContent(result) .build()) .build()); } else if (event.isSessionStatusIdle() && event.asSessionStatusIdle().stopReason().isEndTurn()) { break; } } } ``` ```php PHP use Anthropic\Client; use Anthropic\Beta\Agents\BetaManagedAgentsCustomToolInputSchema; use Anthropic\Beta\Agents\BetaManagedAgentsCustomToolParams; use Anthropic\Beta\Sessions\BetaManagedAgentsAgentParams; $client = new Client(); $agent = $client->beta->agents->create( name: 'weather-agent', model: 'claude-opus-5', system: 'You are a concise weather assistant.', tools: [ BetaManagedAgentsCustomToolParams::with( type: 'custom', name: 'get_weather', description: 'Get the current weather for a city.', inputSchema: BetaManagedAgentsCustomToolInputSchema::with( properties: ['city' => ['type' => 'string']], required: ['city'], ), ), ], ); $environment = $client->beta->environments->create( name: 'weather-env', config: ['type' => 'cloud', 'networking' => ['type' => 'unrestricted']], ); $session = $client->beta->sessions->create( agent: BetaManagedAgentsAgentParams::with( type: 'agent', id: $agent->id, version: $agent->version, ), environmentID: $environment->id, ); function getWeather(string $city): string { return "{$city}: 18°C, clear"; } $stream = $client->beta->sessions->events->streamStream($session->id); $client->beta->sessions->events->send( $session->id, events: [ [ 'type' => 'user.message', 'content' => [['type' => 'text', 'text' => "What's the weather in Tokyo?"]], ], ], ); foreach ($stream as $event) { if ($event->type === 'agent.message') { foreach ($event->content as $block) { if ($block->type === 'text') { echo $block->text . "\n"; } } } elseif ($event->type === 'agent.custom_tool_use') { $result = getWeather($event->input['city']); $client->beta->sessions->events->send( $session->id, events: [ [ 'type' => 'user.custom_tool_result', 'custom_tool_use_id' => $event->id, 'content' => [['type' => 'text', 'text' => $result]], ], ], ); } elseif ($event->type === 'session.status_idle' && $event->stopReason?->type === 'end_turn') { break; } } $stream->close(); ``` ```ruby Ruby require "anthropic" client = Anthropic::Client.new agent = client.beta.agents.create( name: "weather-agent", model: "claude-opus-5", system_: "You are a concise weather assistant.", tools: [ { type: "custom", name: "get_weather", description: "Get the current weather for a city.", input_schema: { type: "object", properties: {city: {type: "string"}}, required: ["city"] } } ] ) environment = client.beta.environments.create( name: "weather-env", config: {type: "cloud", networking: {type: "unrestricted"}} ) session = client.beta.sessions.create( agent: {type: "agent", id: agent.id, version: agent.version}, environment_id: environment.id ) def get_weather(city) "#{city}: 18°C, clear" end stream = client.beta.sessions.events.stream_events(session.id) client.beta.sessions.events.send_( session.id, events: [{type: "user.message", content: [{type: "text", text: "What's the weather in Tokyo?"}]}] ) stream.each do |event| case event.type when :"agent.message" event.content.each do |block| puts block.text if block.type == :text end when :"agent.custom_tool_use" result = get_weather(event.input[:city]) client.beta.sessions.events.send_( session.id, events: [ { type: "user.custom_tool_result", custom_tool_use_id: event.id, content: [{type: "text", text: result}] } ] ) when :"session.status_idle" break if event.stop_reason&.type == :end_turn end end ``` The Agent and Environment are created once and reused across sessions. The tool function still runs in your process; the difference is that you read the `agent.custom_tool_use` event and send the result explicitly instead of the SDK dispatching it for you. ### Features that move to your client The tradeoff for Anthropic running the agent loop is that a few things the SDK handled automatically become your client's responsibility. | SDK feature | Managed Agents approach | | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Plan mode | Run a planning-only session first, then a second session to run the plan. | | Output styles, slash commands | Apply in your client before sending `user.message` or after receiving `agent.message`. | | `PreToolUse` / `PostToolUse` hooks | Your client already sees every `agent.custom_tool_use` event before responding; put the logic there. For built-in tools, use `permission_policy: always_ask`. | | `max_turns` | Count turns client-side. | ## Migration checklist 1. [Create an environment](https://platform.claude.com/docs/en/managed-agents/environments) with the networking and runtimes your agent needs. 2. Port your system prompt and tool selection to an [agent definition](https://platform.claude.com/docs/en/managed-agents/agent-setup). 3. Replace your loop with [`sessions.create`](https://platform.claude.com/docs/en/managed-agents/sessions) and [`sessions.events.stream`](https://platform.claude.com/docs/en/managed-agents/events-and-streaming). 4. For any local files the agent reads, upload them through the [Files API](https://platform.claude.com/docs/en/managed-agents/files) and mount them as `resources`. 5. For any custom tool handlers, move execution into your event loop as responses to `agent.custom_tool_use` events. 6. Verify with a test session before pointing production traffic at the new flow. ## Migrating between model versions When a new Claude model is released, migrating a Claude Managed Agents integration is typically a one-field change: update `model` on your [agent definition](https://platform.claude.com/docs/en/managed-agents/agent-setup) and the change takes effect on the next session you create. ```bash cURL curl -sS --fail-with-body "https://api.anthropic.com/v1/agents/$AGENT_ID?beta=true" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ --json "$(jq -n --argjson version "$AGENT_VERSION" '{version: $version, model: "claude-opus-5"}')" ``` ```bash CLI ant beta:agents update \ --agent-id "$AGENT_ID" \ --version "$AGENT_VERSION" \ --model claude-opus-5 ``` ```python Python client.beta.agents.update( agent.id, version=agent.version, model="claude-opus-5", ) ``` ```typescript TypeScript await client.beta.agents.update(agent.id, { version: agent.version, model: "claude-opus-5" }); ``` ```csharp C# await client.Beta.Agents.Update(agent.ID, new() { Version = agent.Version, Model = BetaManagedAgentsModel.ClaudeOpus5, }); ``` ```go Go _, err = client.Beta.Agents.Update(ctx, agent.ID, anthropic.BetaAgentUpdateParams{ Version: agent.Version, Model: anthropic.BetaManagedAgentsModelConfigParams{ ID: anthropic.BetaManagedAgentsModelClaudeOpus5, }, }) if err != nil { panic(err) } ``` ```java Java client.beta().agents().update( agent.id(), AgentUpdateParams.builder() .version(agent.version()) .model(BetaManagedAgentsModel.CLAUDE_OPUS_5) .build() ); ``` ```php PHP $client->beta->agents->update( $agent->id, version: $agent->version, model: 'claude-opus-5', ); ``` ```ruby Ruby client.beta.agents.update( agent.id, version: agent.version, model: "claude-opus-5" ) ``` Most model-level behavior changes documented in the [Messages API migration guide](https://platform.claude.com/docs/en/about-claude/models/migration-guide) do not require action on your side: * **Request parameter changes** (`max_tokens` defaults, `thinking` configuration) are handled by the Claude Managed Agents runtime. These fields are not exposed on the agent definition. * **Assistant message prefilling** does not exist in the event-based session model, so its removal on newer models is a no-op. * **Tool argument JSON escaping** is parsed by the runtime before you receive `agent.custom_tool_use` events. You see structured data, not raw strings. The behavior descriptions in the Messages API guide (what the model does differently) still apply. The migration steps (how to change your request code) do not. ### Define your agent --- title: Define your agent url: https://platform.claude.com/docs/en/managed-agents/agent-setup description: Create a reusable, versioned agent configuration. --- An agent is a reusable, versioned configuration that defines persona and capabilities. It bundles the model, system prompt, tools, MCP servers, and skills that shape how Claude behaves during a session. Create the agent once as a reusable resource and reference it by ID each time you [start a session](https://platform.claude.com/docs/en/managed-agents/sessions). Agents are versioned and easier to manage across many sessions. Managed Agents API requests require the `managed-agents-2026-04-01` beta header, except memory store endpoints, which use `agent-memory-2026-07-22` instead. The SDK sets the correct beta header automatically. See [Beta headers](https://platform.claude.com/docs/en/api/beta-headers#endpoint-specific-headers). ## Agent configuration fields | Field | Description | | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | Required. A human-readable name for the agent. | | `model` | Required. The Claude [model](https://platform.claude.com/docs/en/about-claude/models/overview) that powers the agent. Accepts a model ID string or an object, for example `{"id": "claude-opus-5"}`. Claude 4.5 and later models are supported. The object form also accepts `speed`, `effort`, and `inference_geo` fields; see the tips under [Create an agent](https://platform.claude.com/docs/en/managed-agents/agent-setup#create-an-agent), [Effort levels](https://platform.claude.com/docs/en/build-with-claude/effort#effort-levels), and [Pin the inference geo](https://platform.claude.com/docs/en/managed-agents/agent-setup#pin-the-inference-geo). | | `system` | A [system prompt](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-prompting-best-practices#give-claude-a-role) that defines the agent's behavior and persona. The system prompt is distinct from [user messages](https://platform.claude.com/docs/en/managed-agents/reference#event-types), which should describe the work to be done. | | `tools` | The tools available to the agent. Combines [pre-built agent tools](https://platform.claude.com/docs/en/managed-agents/tools), [MCP tools](https://platform.claude.com/docs/en/managed-agents/mcp-connector), and [custom tools](https://platform.claude.com/docs/en/managed-agents/tools#custom-tools). | | `mcp_servers` | [MCP servers](https://platform.claude.com/docs/en/managed-agents/mcp-connector) that provide standardized third-party capabilities. | | `skills` | [Skills](https://platform.claude.com/docs/en/managed-agents/skills) that supply domain-specific context with progressive disclosure. | | `multiagent` | A coordinator declaration listing the agents this agent can delegate to. See [Multiagent orchestration](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration). | | `description` | A description of what the agent does. | | `metadata` | Arbitrary key-value pairs for your own tracking. | You can also override `model`, `system`, `tools`, `mcp_servers`, and `skills` for a single session without changing the agent. An `effort` level set inside a per-session `model` override isn't applied, and because the override replaces the agent's `model` object in full, a session created with a `model` override runs at the model's default effort level; to run at a specific effort level, set `effort` on the agent and don't override `model` for that session. See [Override agent configuration for a session](https://platform.claude.com/docs/en/managed-agents/sessions#override-agent-configuration-for-a-session). ## Create an agent The following example defines a coding agent that uses Claude Opus 5 with access to the pre-built agent toolset. The toolset lets the agent write code, read files, search the web, and more. See the [agent tools reference](https://platform.claude.com/docs/en/managed-agents/tools) for the full list of supported tools. The examples use curl, the `ant` CLI, or one of the SDKs. If you haven't set one up, the [quickstart](https://platform.claude.com/docs/en/managed-agents/quickstart#install-the-cli) covers installation and client setup. ```bash cURL agent=$(curl -fsSL https://api.anthropic.com/v1/agents \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d '{ "name": "Coding Assistant", "model": "claude-opus-5", "system": "You are a helpful coding agent.", "tools": [{"type": "agent_toolset_20260401"}] }') AGENT_ID=$(jq -r '.id' <<< "$agent") AGENT_VERSION=$(jq -r '.version' <<< "$agent") ``` ```bash CLI agent=$(ant beta:agents create \ --name "Coding Assistant" \ --model '{id: claude-opus-5}' \ --system "You are a helpful coding agent." \ --tool '{type: agent_toolset_20260401}' \ --format json) AGENT_ID=$(jq -r '.id' <<< "$agent") AGENT_VERSION=$(jq -r '.version' <<< "$agent") ``` ```python Python agent = client.beta.agents.create( name="Coding Assistant", model="claude-opus-5", system="You are a helpful coding agent.", tools=[ {"type": "agent_toolset_20260401"}, ], ) ``` ```typescript TypeScript const agent = await client.beta.agents.create({ name: "Coding Assistant", model: "claude-opus-5", system: "You are a helpful coding agent.", tools: [{ type: "agent_toolset_20260401" }], }); ``` ```csharp C# var agent = await client.Beta.Agents.Create(new() { Name = "Coding Assistant", Model = BetaManagedAgentsModel.ClaudeOpus5, System = "You are a helpful coding agent.", Tools = [ new BetaManagedAgentsAgentToolset20260401Params { Type = "agent_toolset_20260401", }, ], }); ``` ```go Go agent, err := client.Beta.Agents.New(ctx, anthropic.BetaAgentNewParams{ Name: "Coding Assistant", Model: anthropic.BetaManagedAgentsModelConfigParams{ ID: anthropic.BetaManagedAgentsModelClaudeOpus5, }, System: anthropic.String("You are a helpful coding agent."), Tools: []anthropic.BetaAgentNewParamsToolUnion{{ OfAgentToolset20260401: &anthropic.BetaManagedAgentsAgentToolset20260401Params{ Type: anthropic.BetaManagedAgentsAgentToolset20260401ParamsTypeAgentToolset20260401, }, }}, }) if err != nil { panic(err) } ``` ```java Java var agent = client.beta().agents().create( AgentCreateParams.builder() .name("Coding Assistant") .model(BetaManagedAgentsModel.CLAUDE_OPUS_5) .system("You are a helpful coding agent.") .addTool( BetaManagedAgentsAgentToolset20260401Params.builder() .type(BetaManagedAgentsAgentToolset20260401Params.Type.AGENT_TOOLSET_20260401) .build() ) .build() ); ``` ```php PHP $agent = $client->beta->agents->create( name: 'Coding Assistant', model: 'claude-opus-5', system: 'You are a helpful coding agent.', tools: [ BetaManagedAgentsAgentToolset20260401Params::with( type: 'agent_toolset_20260401', ), ], ); ``` ```ruby Ruby agent = client.beta.agents.create( name: "Coding Assistant", model: "claude-opus-5", system_: "You are a helpful coding agent.", tools: [{type: "agent_toolset_20260401"}] ) ``` The response echoes your configuration and adds `id`, `type`, `version`, `created_at`, `updated_at`, and `archived_at` fields, and fills in `model` fields you omit, such as `effort`, with their defaults. The `version` starts at 1 and increments each time an update changes the agent. ```json { "id": "agent_01HqR2k7vXbZ9mNpL3wYcT8f", "type": "agent", "name": "Coding Assistant", "model": { "id": "claude-opus-5", "effort": { "type": "high" }, "speed": "standard" }, "system": "You are a helpful coding agent.", "description": null, "tools": [ { "type": "agent_toolset_20260401", "default_config": { "permission_policy": { "type": "always_allow" } } } ], "skills": [], "mcp_servers": [], "multiagent": null, "metadata": {}, "version": 1, "created_at": "2026-04-03T18:24:10.412Z", "updated_at": "2026-04-03T18:24:10.412Z", "archived_at": null } ``` The `default_config` on the toolset shows its default [permission policy](https://platform.claude.com/docs/en/managed-agents/permission-policies), `always_allow`, which applies unless you configure one. To use Claude Opus 5 or Claude Opus 4.8 with [fast mode](https://platform.claude.com/docs/en/build-with-claude/fast-mode), pass `model` as an object, for example: `{"id": "claude-opus-5", "speed": "fast"}`. See the fast mode page's [supported models](https://platform.claude.com/docs/en/build-with-claude/fast-mode#supported-models). To set the model's effort level, pass `model` as an object, for example: `{"id": "claude-opus-5", "effort": "high"}`. The `effort` field accepts a level string (`low`, `medium`, `high`, `xhigh`, or `max`) or an object such as `{"type": "high"}`. See [Effort levels](https://platform.claude.com/docs/en/build-with-claude/effort#effort-levels) for what each level does. ### Pin the inference geo Like `speed` and `effort`, `inference_geo` is set through the object form of `model`: pass `model` as an object and set `inference_geo` alongside `id`. The field accepts `"us"` or `"global"`. When it's unset, each model request follows the workspace's default inference geo at the time it's served. See [Data residency](https://platform.claude.com/docs/en/manage-claude/data-residency) for the workspace-level geo controls and pricing. The following example pins an agent to US inference and prints the `inference_geo` value echoed in the response's `model` object: ```bash cURL agent=$(curl -fsSL https://api.anthropic.com/v1/agents \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d '{ "name": "Geo-pinned assistant", "model": {"id": "claude-opus-5", "inference_geo": "us"}, "system": "You are a helpful assistant." }') echo "Inference geo: $(jq -r '.model.inference_geo' <<< "$agent")" ``` ```bash CLI agent=$(ant beta:agents create \ --name "Geo-pinned assistant" \ --model '{id: claude-opus-5, inference_geo: us}' \ --system "You are a helpful assistant." \ --format json) echo "Inference geo: $(jq -r '.model.inference_geo' <<< "$agent")" ``` ```python Python agent = client.beta.agents.create( name="Geo-pinned assistant", model={ "id": "claude-opus-5", "inference_geo": "us", }, system="You are a helpful assistant.", ) print(f"Inference geo: {agent.model.inference_geo}") ``` ```typescript TypeScript const agent = await client.beta.agents.create({ name: "Geo-pinned assistant", model: { id: "claude-opus-5", inference_geo: "us" }, system: "You are a helpful assistant.", }); console.log(`Inference geo: ${agent.model.inference_geo}`); ``` ```csharp C# var agent = await client.Beta.Agents.Create(new() { Name = "Geo-pinned assistant", Model = new BetaManagedAgentsModelConfigParams { ID = BetaManagedAgentsModel.ClaudeOpus5, InferenceGeo = "us", }, System = "You are a helpful assistant.", }); Console.WriteLine($"Inference geo: {agent.Model.InferenceGeo}"); ``` ```go Go agent, err := client.Beta.Agents.New(ctx, anthropic.BetaAgentNewParams{ Name: "Geo-pinned assistant", Model: anthropic.BetaManagedAgentsModelConfigParams{ ID: anthropic.BetaManagedAgentsModelClaudeOpus5, InferenceGeo: anthropic.String("us"), }, System: anthropic.String("You are a helpful assistant."), }) if err != nil { panic(err) } fmt.Printf("Inference geo: %s\n", agent.Model.InferenceGeo) ``` ```java Java var agent = client.beta().agents().create( AgentCreateParams.builder() .name("Geo-pinned assistant") .model( BetaManagedAgentsModelConfigParams.builder() .id(BetaManagedAgentsModel.CLAUDE_OPUS_5) .inferenceGeo("us") .build() ) .system("You are a helpful assistant.") .build() ); IO.println("Inference geo: " + agent.model().inferenceGeo().orElseThrow()); ``` ```php PHP $agent = $client->beta->agents->create( name: 'Geo-pinned assistant', model: BetaManagedAgentsModelConfigParams::with( id: 'claude-opus-5', inferenceGeo: 'us', ), system: 'You are a helpful assistant.', ); echo "Inference geo: {$agent->model->inferenceGeo}\n"; ``` ```ruby Ruby agent = client.beta.agents.create( name: "Geo-pinned assistant", model: {id: "claude-opus-5", inference_geo: "us"}, system_: "You are a helpful assistant." ) puts "Inference geo: #{agent.model.inference_geo}" ``` An `inference_geo` pin is validated against the workspace's [`allowed_inference_geos`](https://platform.claude.com/docs/en/manage-claude/data-residency#workspace-level-restrictions) when the agent is saved, when a session is created from it, and on every turn the session serves. If the workspace allowlist narrows so a pin is no longer allowed, new sessions can't be created from the agent and running sessions refuse further turns; pins are never exempted, because workspaces rely on them for compliance and data residency. Setting `inference_geo` on a model that doesn't support geographic inference pinning returns a 400 error; see [Model availability](https://platform.claude.com/docs/en/manage-claude/data-residency#model-availability) for the models that do. In a `multiagent` configuration, the coordinator's pin and every roster member's must all be set to the same value or all be unset; see [Multiagent orchestration](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration). To change or clear the pin later, update the agent's `model` object; supplying `model` without `inference_geo` clears it, as described under [Update semantics](https://platform.claude.com/docs/en/managed-agents/agent-setup#update-semantics). ## Update an agent Updating an agent generates a new version when the configuration changes. The `version` field is optional: supply it for optimistic concurrency (a mismatch returns a 409), or omit it to apply the update unconditionally (last write wins). Updates to archived agents are rejected. ```bash cURL updated_agent=$(curl -fsSL "https://api.anthropic.com/v1/agents/$AGENT_ID" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d @- <beta->agents->update( $agent->id, version: $agent->version, system: 'You are a helpful coding agent. Always write tests.', ); echo "New version: {$updatedAgent->version}\n"; ``` ```ruby Ruby updated_agent = client.beta.agents.update( agent.id, version: agent.version, system_: "You are a helpful coding agent. Always write tests." ) puts "New version: #{updated_agent.version}" ``` The preceding example supplies `version` from the create response, so the update only applies if nothing else has changed the agent since you read it. To apply an update unconditionally, omit `version` from the request: ```bash cURL updated_agent=$(curl -fsSL "https://api.anthropic.com/v1/agents/$AGENT_ID" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d '{ "description": "Writes and reviews code." }') echo "New version: $(jq -r '.version' <<< "$updated_agent")" ``` ### Update semantics * **`version`** is optional and must be at least 1 when supplied. When supplied, the request returns a 409 if it doesn't match the agent's current version, even when the fields you send already match the stored values; re-read the agent and retry. When omitted, the update applies unconditionally and the most recent update silently replaces any concurrent one, with no error to either caller. Supplying `version` is the recommended default for interactive callers, and omitting it fits declarative apply loops, such as a CI job that syncs checked-in agent definitions, where the loop owns the agent. * **Omitted fields are preserved.** You only need to include the fields you want to change. * **Scalar fields** (`model`, `system`, `name`, `description`) are replaced with the new value. `system` and `description` can be cleared by passing `null`. `model` and `name` are mandatory and cannot be cleared. Within a `model` object you supply, `effort` is the sole exception: if the model `id` is unchanged, omitting `effort` leaves the stored effort level unchanged. If you change the model `id`, an omitted `effort` resets to the new model's default. Other `model` fields are replaced along with the object: supplying `model` without `inference_geo` clears the agent's inference geo pin. * **Array fields** (`tools`, `mcp_servers`, `skills`) are fully replaced by the new array. To clear an array field entirely, pass `null` or an empty array. * **`multiagent`** is replaced as a whole, including its `agents` roster. Pass `null` to clear it. * **Metadata** is merged at the key level. Keys you provide are added or updated. Keys you omit are preserved. To delete a specific key, set its value to `null`. * **No-op detection.** If the update produces no change relative to the current version, no new version is created and the existing version is returned. * **Coordinator rosters are not updated.** Coordinators that reference this agent in their `multiagent.agents` roster keep the version that was pinned when the coordinator was created or last updated, even if the reference omits `version`. To delegate to the new version, [update the coordinator](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration#configure-the-coordinator) so its roster references it. ## Agent lifecycle | Operation | Behavior | | ----------------- | --------------------------------------------------------------------------------------------------- | | **Update** | Generates a new agent version when the configuration changes. | | **List versions** | Returns the full version history so you can track changes over time. | | **Archive** | Makes the agent read-only. New sessions cannot reference it, but existing sessions continue to run. | ### List versions Fetch the full version history to track how an agent has changed over time. Results are paginated, and the SDK examples fetch every page automatically. ```bash cURL curl -fsSL "https://api.anthropic.com/v1/agents/$AGENT_ID/versions" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ | jq -r '.data[] | "Version \(.version): \(.updated_at)"' ``` ```bash CLI ant beta:agents:versions list --agent-id "$AGENT_ID" ``` ```python Python for version in client.beta.agents.versions.list(agent.id): print(f"Version {version.version}: {version.updated_at.isoformat()}") ``` ```typescript TypeScript for await (const version of client.beta.agents.versions.list(agent.id)) { console.log(`Version ${version.version}: ${version.updated_at}`); } ``` ```csharp C# var versions = await client.Beta.Agents.Versions.List(agent.ID); await foreach (var version in versions.Paginate()) { Console.WriteLine($"Version {version.Version}: {version.UpdatedAt:O}"); } ``` ```go Go iter := client.Beta.Agents.Versions.ListAutoPaging(ctx, agent.ID, anthropic.BetaAgentVersionListParams{}) for iter.Next() { version := iter.Current() fmt.Printf("Version %d: %s\n", version.Version, version.UpdatedAt.Format(time.RFC3339)) } if err := iter.Err(); err != nil { panic(err) } ``` ```java Java for (var version : client.beta().agents().versions().list(agent.id()).autoPager()) { IO.println("Version " + version.version() + ": " + version.updatedAt()); } ``` ```php PHP foreach ($client->beta->agents->versions->list($agent->id)->pagingEachItem() as $version) { echo "Version {$version->version}: {$version->updatedAt->format(DateTimeInterface::ATOM)}\n"; } ``` ```ruby Ruby client.beta.agents.versions.list(agent.id).auto_paging_each do |agent_version| puts "Version #{agent_version.version}: #{agent_version.updated_at.iso8601}" end ``` ### Archive an agent Archiving makes the agent read-only and cannot be undone. Existing sessions continue to run, but new sessions cannot reference the agent. The response sets `archived_at` to the archive timestamp. ```bash cURL archived=$(curl -fsSL -X POST "https://api.anthropic.com/v1/agents/$AGENT_ID/archive" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01") echo "Archived at: $(jq -r '.archived_at' <<< "$archived")" ``` ```bash CLI ant beta:agents archive --agent-id "$AGENT_ID" ``` ```python Python archived = client.beta.agents.archive(agent.id) print(f"Archived at: {archived.archived_at.isoformat()}") ``` ```typescript TypeScript const archived = await client.beta.agents.archive(agent.id); console.log(`Archived at: ${archived.archived_at}`); ``` ```csharp C# var archived = await client.Beta.Agents.Archive(agent.ID); Console.WriteLine($"Archived at: {archived.ArchivedAt:O}"); ``` ```go Go archived, err := client.Beta.Agents.Archive(ctx, agent.ID, anthropic.BetaAgentArchiveParams{}) if err != nil { panic(err) } fmt.Printf("Archived at: %s\n", archived.ArchivedAt.Format(time.RFC3339)) ``` ```java Java var archived = client.beta().agents().archive(agent.id()); IO.println("Archived at: " + archived.archivedAt().orElseThrow()); ``` ```php PHP $archived = $client->beta->agents->archive($agent->id); echo "Archived at: {$archived->archivedAt->format(DateTimeInterface::ATOM)}\n"; ``` ```ruby Ruby archived = client.beta.agents.archive(agent.id) puts "Archived at: #{archived.archived_at.iso8601}" ``` ## Next steps Configure tools available to your agent. Attach reusable, filesystem-based expertise to your agent for domain-specific workflows. Create a session to run your agent and begin executing tasks. Event types, self-hosted worker CLI flags, supported MCP server types, rate limits, and branding guidelines for Claude Managed Agents. --- title: MCP connector url: https://platform.claude.com/docs/en/managed-agents/mcp-connector description: Connect MCP servers to your agents for access to external tools and data sources. --- Claude Managed Agents supports connecting [Model Context Protocol (MCP)](https://modelcontextprotocol.io) servers to your agents. This gives the agent access to external tools, data sources, and services through a standardized protocol. MCP configuration is split across two steps: 1. **Agent creation** declares which MCP servers the agent connects to, by name and URL. 2. **Session creation** supplies authentication for those servers by referencing a pre-registered vault (see [Authenticate with vaults](https://platform.claude.com/docs/en/managed-agents/vaults)). This separation keeps secrets out of reusable agent definitions while letting each session authenticate with its own credentials. Managed Agents API requests require the `managed-agents-2026-04-01` beta header, except memory store endpoints, which use `agent-memory-2026-07-22` instead. The SDK sets the correct beta header automatically. See [Beta headers](https://platform.claude.com/docs/en/api/beta-headers#endpoint-specific-headers). ## Declare MCP servers on the agent Specify MCP servers in the `mcp_servers` array when creating an agent. Each server needs a `type`, a unique `name`, and a `url`. No authentication tokens are provided at this stage. Each declared server also needs a matching `mcp_toolset` entry in the `tools` array. The toolset's `mcp_server_name` must match the server's `name`. ```bash cURL agent_response=$(curl -sS --fail-with-body https://api.anthropic.com/v1/agents \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d @- <<'EOF' { "name": "GitHub Assistant", "model": "claude-opus-5", "mcp_servers": [ { "type": "url", "name": "github", "url": "https://api.githubcopilot.com/mcp/" } ], "tools": [ {"type": "agent_toolset_20260401"}, {"type": "mcp_toolset", "mcp_server_name": "github"} ] } EOF ) agent_id=$(jq -r '.id' <<<"$agent_response") ``` ```bash CLI AGENT_ID=$(ant beta:agents create \ --name "GitHub Assistant" \ --model '{id: claude-opus-5}' \ --mcp-server '{type: url, name: github, url: "https://api.githubcopilot.com/mcp/"}' \ --tool '{type: agent_toolset_20260401}' \ --tool '{type: mcp_toolset, mcp_server_name: github}' \ --transform id --raw-output) ``` ```python Python agent = client.beta.agents.create( name="GitHub Assistant", model="claude-opus-5", mcp_servers=[ { "type": "url", "name": "github", "url": "https://api.githubcopilot.com/mcp/", }, ], tools=[ {"type": "agent_toolset_20260401"}, {"type": "mcp_toolset", "mcp_server_name": "github"}, ], ) ``` ```typescript TypeScript const agent = await client.beta.agents.create({ name: "GitHub Assistant", model: "claude-opus-5", mcp_servers: [ { type: "url", name: "github", url: "https://api.githubcopilot.com/mcp/", }, ], tools: [ { type: "agent_toolset_20260401" }, { type: "mcp_toolset", mcp_server_name: "github" }, ], }); ``` ```csharp C# var agent = await client.Beta.Agents.Create(new() { Name = "GitHub Assistant", Model = BetaManagedAgentsModel.ClaudeOpus5, McpServers = [ new() { Type = "url", Name = "github", Url = "https://api.githubcopilot.com/mcp/" }, ], Tools = [ new BetaManagedAgentsAgentToolset20260401Params { Type = "agent_toolset_20260401", }, new BetaManagedAgentsMcpToolsetParams { Type = "mcp_toolset", McpServerName = "github" }, ], }); ``` ```go Go agent, err := client.Beta.Agents.New(ctx, anthropic.BetaAgentNewParams{ Name: "GitHub Assistant", Model: anthropic.BetaManagedAgentsModelConfigParams{ ID: anthropic.BetaManagedAgentsModelClaudeOpus5, }, MCPServers: []anthropic.BetaManagedAgentsURLMCPServerParams{{ Type: anthropic.BetaManagedAgentsURLMCPServerParamsTypeURL, Name: "github", URL: "https://api.githubcopilot.com/mcp/", }}, Tools: []anthropic.BetaAgentNewParamsToolUnion{ { OfAgentToolset20260401: &anthropic.BetaManagedAgentsAgentToolset20260401Params{ Type: anthropic.BetaManagedAgentsAgentToolset20260401ParamsTypeAgentToolset20260401, }, }, { OfMCPToolset: &anthropic.BetaManagedAgentsMCPToolsetParams{ Type: anthropic.BetaManagedAgentsMCPToolsetParamsTypeMCPToolset, MCPServerName: "github", }, }, }, }) if err != nil { panic(err) } ``` ```java Java var agent = client.beta().agents().create( AgentCreateParams.builder() .name("GitHub Assistant") .model(BetaManagedAgentsModel.CLAUDE_OPUS_5) .addMcpServer( BetaManagedAgentsUrlMcpServerParams.builder() .type(BetaManagedAgentsUrlMcpServerParams.Type.URL) .name("github") .url("https://api.githubcopilot.com/mcp/") .build() ) .addTool( BetaManagedAgentsAgentToolset20260401Params.builder() .type(BetaManagedAgentsAgentToolset20260401Params.Type.AGENT_TOOLSET_20260401) .build() ) .addTool( BetaManagedAgentsMcpToolsetParams.builder() .type(BetaManagedAgentsMcpToolsetParams.Type.MCP_TOOLSET) .mcpServerName("github") .build() ) .build() ); ``` ```php PHP $agent = $client->beta->agents->create( name: 'GitHub Assistant', model: 'claude-opus-5', mcpServers: [ BetaManagedAgentsURLMCPServerParams::with( type: 'url', name: 'github', url: 'https://api.githubcopilot.com/mcp/', ), ], tools: [ BetaManagedAgentsAgentToolset20260401Params::with( type: 'agent_toolset_20260401', ), BetaManagedAgentsMCPToolsetParams::with( type: 'mcp_toolset', mcpServerName: 'github', ), ], ); ``` ```ruby Ruby agent = client.beta.agents.create( name: "GitHub Assistant", model: "claude-opus-5", mcp_servers: [ { type: "url", name: "github", url: "https://api.githubcopilot.com/mcp/" } ], tools: [ {type: "agent_toolset_20260401"}, {type: "mcp_toolset", mcp_server_name: "github"} ] ) ``` The MCP toolset defaults to a permission policy of `always_ask`, which requires user approval before each tool call. See [permission policies](https://platform.claude.com/docs/en/managed-agents/permission-policies) to configure this behavior. ### `mcp_servers` field reference Each entry in the `mcp_servers` array defines one connection. | Field | Description | | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `type` | Required. Must be `"url"`. | | `name` | Required. A unique name for this server within the agent (1–255 characters). Used as the `mcp_server_name` in the `tools` array and surfaced on MCP tool events in the [session event stream](https://platform.claude.com/docs/en/managed-agents/events-and-streaming). | | `url` | Required. The endpoint of the remote MCP server (up to 2,048 characters). See [Supported MCP server types](https://platform.claude.com/docs/en/managed-agents/reference#supported-mcp-server-types) for transport requirements. | Constraints: * An agent can declare up to 20 MCP servers. Server names must be unique within the array. * Every `mcp_servers` entry must be referenced by an `mcp_toolset` in the `tools` array, and every `mcp_toolset` must reference a declared server. The API rejects agent definitions with unreferenced servers or dangling toolsets. ## Configure which MCP tools are available The `mcp_toolset` entry supports the same `default_config` and `configs` shape as the built-in agent toolset, applied to the tools the MCP server exposes. The `name` in each `configs` entry is the bare tool name as reported by the server. By default all tools exposed by the MCP server are enabled. To enable only specific tools, set `default_config.enabled` to `false` and explicitly enable the tools you want: ```json { "type": "mcp_toolset", "mcp_server_name": "github", "default_config": { "enabled": false }, "configs": [ { "name": "get_issue", "enabled": true }, { "name": "list_issues", "enabled": true }, { "name": "add_issue_comment", "enabled": true } ] } ``` This pattern is useful when a server exposes many tools but the agent only needs a few, or when you want tools added by the server operator to stay off until you review them. To disable specific tools while keeping the rest enabled, omit `default_config` and set `enabled: false` on individual entries: ```json { "type": "mcp_toolset", "mcp_server_name": "github", "configs": [{ "name": "delete_repository", "enabled": false }] } ``` See [configuring the toolset](https://platform.claude.com/docs/en/managed-agents/tools#configuring-the-toolset) for the general `default_config` / `configs` pattern, and [MCP toolset permissions](https://platform.claude.com/docs/en/managed-agents/permission-policies#mcp-toolset-permissions) for setting `permission_policy` on MCP tools and handling confirmation requests. ### MCP tool output handling When an MCP tool output exceeds 100,000 characters (about 25,000 tokens), it is automatically written to a file in the sandbox. The model receives a truncated preview with the file path and can read the full content from there. ## Provide authentication at session creation When starting a session, pass `vault_ids` to provide credentials for your MCP servers. Vaults are collections of credentials that you register once and reference by ID. See [Authenticate with vaults](https://platform.claude.com/docs/en/managed-agents/vaults) for how to create vaults and manage credentials. ```bash cURL session_response=$(curl -sS --fail-with-body https://api.anthropic.com/v1/sessions \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d @- <beta->sessions->create( agent: $agent->id, environmentID: $environment->id, vaultIDs: [$vault->id], ); ``` ```ruby Ruby session = client.beta.sessions.create( agent: agent.id, environment_id: environment.id, vault_ids: [vault.id] ) ``` Credentials are matched by URL, so the vault must contain a credential whose `mcp_server_url` refers to the same server as the `url` declared in `mcp_servers`. Both URLs are normalized before matching (scheme and host lowercased, default ports and trailing slashes stripped), so differences in host casing, a default port, or a trailing slash don't prevent a match; a different path, subdomain, or non-default port does. If none matches, the connection is attempted unauthenticated. See [Add a credential](https://platform.claude.com/docs/en/managed-agents/vaults#add-a-credential) for the `static_bearer` and `mcp_oauth` credential types. ### Handle connection and authentication failures Session creation does not validate MCP connectivity or credentials. If an MCP server is unreachable or rejects the supplied credential, the session still starts and interaction remains possible. A [`session.error`](https://platform.claude.com/docs/en/managed-agents/events-and-streaming) event is emitted with the `mcp_server_name` of the affected server and a `retry_status`: | Error type | Meaning | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `mcp_connection_failed_error` | The MCP server could not be reached (network error, timeout, or non-authentication HTTP failure). | | `mcp_authentication_failed_error` | Authentication with the MCP server failed: the server rejected the credential from the attached vault, required authentication when no matching credential was configured, or an OAuth token refresh failed. | You can decide whether to block further interaction on this error, trigger a credential rotation, or let the session continue without the affected server's tools. The connection is retried on the next `session.status_idle` to `session.status_running` transition. ## Next steps Control when agent and MCP tools run. Send events, stream responses, and interrupt or redirect your session mid-execution. Transport requirements for remote MCP servers. --- title: Permission policies url: https://platform.claude.com/docs/en/managed-agents/permission-policies description: Control when agent and MCP tools execute. --- Permission policies control whether server-executed tools (the pre-built agent toolset and MCP toolset) run automatically or wait for your approval. Custom tools are executed by your application and controlled by you, so they are not governed by permission policies. Managed Agents API requests require the `managed-agents-2026-04-01` beta header, except memory store endpoints, which use `agent-memory-2026-07-22` instead. The SDK sets the correct beta header automatically. See [Beta headers](https://platform.claude.com/docs/en/api/beta-headers#endpoint-specific-headers). ## Permission policy types | Policy | Behavior | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `always_allow` | The tool executes automatically with no confirmation. | | `always_ask` | The session pauses and waits for your approval before executing. See [Respond to confirmation requests](https://platform.claude.com/docs/en/managed-agents/permission-policies#respond-to-confirmation-requests) for the event flow. | Each toolset kind has its own default: the agent toolset defaults to `always_allow`, and MCP toolsets default to `always_ask`. A permission policy controls when an enabled tool runs. To remove a tool from the agent entirely, disable it instead. See [Disabling specific tools](https://platform.claude.com/docs/en/managed-agents/tools#disabling-specific-tools). ## Set a policy for a toolset You set permission policies in the agent's `tools` configuration when you create the agent, and you can change them later by [updating the agent](https://platform.claude.com/docs/en/managed-agents/agent-setup#update-an-agent). Running sessions keep the toolset configuration they were created with. Updates apply to sessions created afterward. ### Agent toolset permissions When creating an agent, you can apply a policy to every tool in `agent_toolset_20260401` using `default_config.permission_policy`: ```bash cURL agent=$(curl -fsSL https://api.anthropic.com/v1/agents \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d '{ "name": "Coding Assistant", "model": "claude-opus-5", "tools": [ { "type": "agent_toolset_20260401", "default_config": { "permission_policy": {"type": "always_ask"} } } ] }') ``` ```bash CLI ant beta:agents create <<'YAML' name: Coding Assistant model: claude-opus-5 tools: - type: agent_toolset_20260401 default_config: permission_policy: type: always_ask YAML ``` ```python Python agent = client.beta.agents.create( name="Coding Assistant", model="claude-opus-5", tools=[ { "type": "agent_toolset_20260401", "default_config": { "permission_policy": {"type": "always_ask"}, }, }, ], ) ``` ```typescript TypeScript const agent = await client.beta.agents.create({ name: "Coding Assistant", model: "claude-opus-5", tools: [ { type: "agent_toolset_20260401", default_config: { permission_policy: { type: "always_ask" } } } ] }); ``` ```csharp C# using Anthropic.Models.Beta.Agents; var agent = await client.Beta.Agents.Create(new() { Name = "Coding Assistant", Model = BetaManagedAgentsModel.ClaudeOpus5, Tools = [ new BetaManagedAgentsAgentToolset20260401Params { Type = BetaManagedAgentsAgentToolset20260401ParamsType.AgentToolset20260401, DefaultConfig = new() { PermissionPolicy = new BetaManagedAgentsAlwaysAskPolicy { Type = "always_ask" }, }, }, ], }); ``` ```go Go agent, err := client.Beta.Agents.New(ctx, anthropic.BetaAgentNewParams{ Name: "Coding Assistant", Model: anthropic.BetaManagedAgentsModelConfigParams{ ID: "claude-opus-5", }, Tools: []anthropic.BetaAgentNewParamsToolUnion{{ OfAgentToolset20260401: &anthropic.BetaManagedAgentsAgentToolset20260401Params{ Type: anthropic.BetaManagedAgentsAgentToolset20260401ParamsTypeAgentToolset20260401, DefaultConfig: anthropic.BetaManagedAgentsAgentToolsetDefaultConfigParams{ PermissionPolicy: anthropic.BetaManagedAgentsAgentToolsetDefaultConfigParamsPermissionPolicyUnion{ OfAlwaysAsk: &anthropic.BetaManagedAgentsAlwaysAskPolicyParam{ Type: anthropic.BetaManagedAgentsAlwaysAskPolicyTypeAlwaysAsk, }, }, }, }, }}, }) if err != nil { panic(err) } _ = agent ``` ```java Java import com.anthropic.models.beta.agents.*; var agent = client.beta().agents().create( AgentCreateParams.builder() .name("Coding Assistant") .model(BetaManagedAgentsModel.CLAUDE_OPUS_5) .addTool( BetaManagedAgentsAgentToolset20260401Params.builder() .type(BetaManagedAgentsAgentToolset20260401Params.Type.AGENT_TOOLSET_20260401) .defaultConfig( BetaManagedAgentsAgentToolsetDefaultConfigParams.builder() .permissionPolicy( BetaManagedAgentsAlwaysAskPolicy.builder() .type(BetaManagedAgentsAlwaysAskPolicy.Type.ALWAYS_ASK) .build() ) .build() ) .build() ) .build() ); ``` ```php PHP use Anthropic\Beta\Agents\BetaManagedAgentsAgentToolset20260401Params; use Anthropic\Beta\Agents\BetaManagedAgentsAgentToolsetDefaultConfigParams; use Anthropic\Beta\Agents\BetaManagedAgentsAlwaysAskPolicy; $agent = $client->beta->agents->create( name: 'Coding Assistant', model: 'claude-opus-5', tools: [ BetaManagedAgentsAgentToolset20260401Params::with( type: 'agent_toolset_20260401', defaultConfig: BetaManagedAgentsAgentToolsetDefaultConfigParams::with( permissionPolicy: BetaManagedAgentsAlwaysAskPolicy::with(type: 'always_ask'), ), ), ], ); ``` ```ruby Ruby agent = client.beta.agents.create( name: "Coding Assistant", model: "claude-opus-5", tools: [ { type: "agent_toolset_20260401", default_config: { permission_policy: {type: "always_ask"} } } ] ) ``` `default_config` is optional. If you omit it, the agent toolset is enabled with the default permission policy, `always_allow`. ### MCP toolset permissions MCP toolsets default to `always_ask`. This ensures that new tools added to an MCP server do not execute in your application without approval. To auto-approve tools from a trusted MCP server, set `default_config.permission_policy` on the `mcp_toolset` entry. The `mcp_server_name` must match the `name` of a server in the `mcp_servers` array. This example connects a GitHub MCP server and allows its tools to run without confirmation: ```bash cURL agent=$(curl -fsSL https://api.anthropic.com/v1/agents \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d '{ "name": "Dev Assistant", "model": "claude-opus-5", "mcp_servers": [ {"type": "url", "name": "github", "url": "https://mcp.example.com/github"} ], "tools": [ {"type": "agent_toolset_20260401"}, { "type": "mcp_toolset", "mcp_server_name": "github", "default_config": { "permission_policy": {"type": "always_allow"} } } ] }') ``` ```bash CLI ant beta:agents create <<'YAML' name: Dev Assistant model: claude-opus-5 mcp_servers: - type: url name: github url: https://mcp.example.com/github tools: - type: agent_toolset_20260401 - type: mcp_toolset mcp_server_name: github default_config: permission_policy: type: always_allow YAML ``` ```python Python agent = client.beta.agents.create( name="Dev Assistant", model="claude-opus-5", mcp_servers=[ {"type": "url", "name": "github", "url": "https://mcp.example.com/github"}, ], tools=[ {"type": "agent_toolset_20260401"}, { "type": "mcp_toolset", "mcp_server_name": "github", "default_config": { "permission_policy": {"type": "always_allow"}, }, }, ], ) ``` ```typescript TypeScript const agent = await client.beta.agents.create({ name: "Dev Assistant", model: "claude-opus-5", mcp_servers: [{ type: "url", name: "github", url: "https://mcp.example.com/github" }], tools: [ { type: "agent_toolset_20260401" }, { type: "mcp_toolset", mcp_server_name: "github", default_config: { permission_policy: { type: "always_allow" } } } ] }); ``` ```csharp C# using Anthropic.Models.Beta.Agents; var agent = await client.Beta.Agents.Create(new() { Name = "Dev Assistant", Model = BetaManagedAgentsModel.ClaudeOpus5, McpServers = [ new() { Type = BetaManagedAgentsUrlMcpServerParamsType.Url, Name = "github", Url = "https://mcp.example.com/github", }, ], Tools = [ new BetaManagedAgentsAgentToolset20260401Params { Type = BetaManagedAgentsAgentToolset20260401ParamsType.AgentToolset20260401, }, new BetaManagedAgentsMcpToolsetParams { Type = BetaManagedAgentsMcpToolsetParamsType.McpToolset, McpServerName = "github", DefaultConfig = new() { PermissionPolicy = new BetaManagedAgentsAlwaysAllowPolicy { Type = "always_allow" }, }, }, ], }); ``` ```go Go agent, err := client.Beta.Agents.New(ctx, anthropic.BetaAgentNewParams{ Name: "Dev Assistant", Model: anthropic.BetaManagedAgentsModelConfigParams{ ID: "claude-opus-5", }, MCPServers: []anthropic.BetaManagedAgentsURLMCPServerParams{{ Type: anthropic.BetaManagedAgentsURLMCPServerParamsTypeURL, Name: "github", URL: "https://mcp.example.com/github", }}, Tools: []anthropic.BetaAgentNewParamsToolUnion{ { OfAgentToolset20260401: &anthropic.BetaManagedAgentsAgentToolset20260401Params{ Type: anthropic.BetaManagedAgentsAgentToolset20260401ParamsTypeAgentToolset20260401, }, }, { OfMCPToolset: &anthropic.BetaManagedAgentsMCPToolsetParams{ Type: anthropic.BetaManagedAgentsMCPToolsetParamsTypeMCPToolset, MCPServerName: "github", DefaultConfig: anthropic.BetaManagedAgentsMCPToolsetDefaultConfigParams{ PermissionPolicy: anthropic.BetaManagedAgentsMCPToolsetDefaultConfigParamsPermissionPolicyUnion{ OfAlwaysAllow: &anthropic.BetaManagedAgentsAlwaysAllowPolicyParam{ Type: anthropic.BetaManagedAgentsAlwaysAllowPolicyTypeAlwaysAllow, }, }, }, }, }, }, }) if err != nil { panic(err) } _ = agent ``` ```java Java import com.anthropic.models.beta.agents.*; var agent = client.beta().agents().create( AgentCreateParams.builder() .name("Dev Assistant") .model(BetaManagedAgentsModel.CLAUDE_OPUS_5) .addMcpServer( BetaManagedAgentsUrlMcpServerParams.builder() .type(BetaManagedAgentsUrlMcpServerParams.Type.URL) .name("github") .url("https://mcp.example.com/github") .build() ) .addTool( BetaManagedAgentsAgentToolset20260401Params.builder() .type(BetaManagedAgentsAgentToolset20260401Params.Type.AGENT_TOOLSET_20260401) .build() ) .addTool( BetaManagedAgentsMcpToolsetParams.builder() .type(BetaManagedAgentsMcpToolsetParams.Type.MCP_TOOLSET) .mcpServerName("github") .defaultConfig( BetaManagedAgentsMcpToolsetDefaultConfigParams.builder() .permissionPolicy( BetaManagedAgentsAlwaysAllowPolicy.builder() .type(BetaManagedAgentsAlwaysAllowPolicy.Type.ALWAYS_ALLOW) .build() ) .build() ) .build() ) .build() ); ``` ```php PHP use Anthropic\Beta\Agents\BetaManagedAgentsAgentToolset20260401Params; use Anthropic\Beta\Agents\BetaManagedAgentsAlwaysAllowPolicy; use Anthropic\Beta\Agents\BetaManagedAgentsMCPToolsetDefaultConfigParams; use Anthropic\Beta\Agents\BetaManagedAgentsMCPToolsetParams; use Anthropic\Beta\Agents\BetaManagedAgentsURLMCPServerParams; $agent = $client->beta->agents->create( name: 'Dev Assistant', model: 'claude-opus-5', mcpServers: [ BetaManagedAgentsURLMCPServerParams::with( type: 'url', name: 'github', url: 'https://mcp.example.com/github', ), ], tools: [ BetaManagedAgentsAgentToolset20260401Params::with( type: 'agent_toolset_20260401', ), BetaManagedAgentsMCPToolsetParams::with( type: 'mcp_toolset', mcpServerName: 'github', defaultConfig: BetaManagedAgentsMCPToolsetDefaultConfigParams::with( permissionPolicy: BetaManagedAgentsAlwaysAllowPolicy::with(type: 'always_allow'), ), ), ], ); ``` ```ruby Ruby agent = client.beta.agents.create( name: "Dev Assistant", model: "claude-opus-5", mcp_servers: [ {type: "url", name: "github", url: "https://mcp.example.com/github"} ], tools: [ {type: "agent_toolset_20260401"}, { type: "mcp_toolset", mcp_server_name: "github", default_config: { permission_policy: {type: "always_allow"} } } ] ) ``` ## Override an individual tool policy Use the `configs` array to override the default for individual tools. The `name` values for the agent toolset are listed in [Available tools](https://platform.claude.com/docs/en/managed-agents/tools#available-tools). This example allows the full agent toolset by default but requires confirmation before any bash command runs: ```bash cURL tools='[ { "type": "agent_toolset_20260401", "default_config": { "permission_policy": {"type": "always_allow"} }, "configs": [ { "name": "bash", "permission_policy": {"type": "always_ask"} } ] } ]' ``` ```bash CLI ant beta:agents create <<'YAML' name: Coding Assistant model: claude-opus-5 tools: - type: agent_toolset_20260401 default_config: permission_policy: type: always_allow configs: - name: bash permission_policy: type: always_ask YAML ``` ```python Python tools = [ { "type": "agent_toolset_20260401", "default_config": { "permission_policy": {"type": "always_allow"}, }, "configs": [ { "name": "bash", "permission_policy": {"type": "always_ask"}, }, ], }, ] ``` ```typescript TypeScript const tools = [ { type: "agent_toolset_20260401", default_config: { permission_policy: { type: "always_allow" } }, configs: [ { name: "bash", permission_policy: { type: "always_ask" } } ] } ] satisfies Anthropic.Beta.AgentCreateParams["tools"]; ``` ```csharp C# using Anthropic.Models.Beta.Agents; using Tool = Anthropic.Models.Beta.Agents.Tool; Tool[] tools = [ new BetaManagedAgentsAgentToolset20260401Params { Type = BetaManagedAgentsAgentToolset20260401ParamsType.AgentToolset20260401, DefaultConfig = new() { PermissionPolicy = new BetaManagedAgentsAlwaysAllowPolicy { Type = "always_allow" }, }, Configs = [ new() { Name = "bash", PermissionPolicy = new BetaManagedAgentsAlwaysAskPolicy { Type = "always_ask" }, }, ], }, ]; ``` ```go Go tools := []anthropic.BetaAgentNewParamsToolUnion{{ OfAgentToolset20260401: &anthropic.BetaManagedAgentsAgentToolset20260401Params{ Type: anthropic.BetaManagedAgentsAgentToolset20260401ParamsTypeAgentToolset20260401, DefaultConfig: anthropic.BetaManagedAgentsAgentToolsetDefaultConfigParams{ PermissionPolicy: anthropic.BetaManagedAgentsAgentToolsetDefaultConfigParamsPermissionPolicyUnion{ OfAlwaysAllow: &anthropic.BetaManagedAgentsAlwaysAllowPolicyParam{ Type: anthropic.BetaManagedAgentsAlwaysAllowPolicyTypeAlwaysAllow, }, }, }, Configs: []anthropic.BetaManagedAgentsAgentToolConfigParams{{ Name: anthropic.BetaManagedAgentsAgentToolConfigParamsNameBash, PermissionPolicy: anthropic.BetaManagedAgentsAgentToolConfigParamsPermissionPolicyUnion{ OfAlwaysAsk: &anthropic.BetaManagedAgentsAlwaysAskPolicyParam{ Type: anthropic.BetaManagedAgentsAlwaysAskPolicyTypeAlwaysAsk, }, }, }}, }, }} _ = tools ``` ```java Java import com.anthropic.models.beta.agents.*; import java.util.List; var tools = List.of( AgentCreateParams.Tool.ofAgentToolset20260401( BetaManagedAgentsAgentToolset20260401Params.builder() .type(BetaManagedAgentsAgentToolset20260401Params.Type.AGENT_TOOLSET_20260401) .defaultConfig( BetaManagedAgentsAgentToolsetDefaultConfigParams.builder() .permissionPolicy( BetaManagedAgentsAlwaysAllowPolicy.builder() .type(BetaManagedAgentsAlwaysAllowPolicy.Type.ALWAYS_ALLOW) .build() ) .build() ) .addConfig( BetaManagedAgentsAgentToolConfigParams.builder() .name(BetaManagedAgentsAgentToolConfigParams.Name.BASH) .permissionPolicy( BetaManagedAgentsAlwaysAskPolicy.builder() .type(BetaManagedAgentsAlwaysAskPolicy.Type.ALWAYS_ASK) .build() ) .build() ) .build() ) ); ``` ```php PHP use Anthropic\Beta\Agents\BetaManagedAgentsAgentToolConfigParams; use Anthropic\Beta\Agents\BetaManagedAgentsAgentToolset20260401Params; use Anthropic\Beta\Agents\BetaManagedAgentsAgentToolsetDefaultConfigParams; use Anthropic\Beta\Agents\BetaManagedAgentsAlwaysAllowPolicy; use Anthropic\Beta\Agents\BetaManagedAgentsAlwaysAskPolicy; $tools = [ BetaManagedAgentsAgentToolset20260401Params::with( type: 'agent_toolset_20260401', defaultConfig: BetaManagedAgentsAgentToolsetDefaultConfigParams::with( permissionPolicy: BetaManagedAgentsAlwaysAllowPolicy::with(type: 'always_allow'), ), configs: [ BetaManagedAgentsAgentToolConfigParams::with( name: 'bash', permissionPolicy: BetaManagedAgentsAlwaysAskPolicy::with(type: 'always_ask'), ), ], ), ]; ``` ```ruby Ruby tools = [ { type: "agent_toolset_20260401", default_config: { permission_policy: {type: "always_allow"} }, configs: [ { name: "bash", permission_policy: {type: "always_ask"} } ] } ] ``` Pass this `tools` configuration in the agent create request (the CLI tab shows the complete command). MCP toolsets support the same per-tool overrides, with `name` set to the tool name reported by the MCP server. See [Configure which MCP tools are available](https://platform.claude.com/docs/en/managed-agents/mcp-connector#configure-which-mcp-tools-are-available). ## Respond to confirmation requests When the agent invokes a tool with an `always_ask` policy: 1. The session emits an `agent.tool_use` or `agent.mcp_tool_use` event. 2. The session pauses with a `session.status_idle` event whose `stop_reason.type` is `requires_action`. The blocking event IDs are in the `stop_reason.event_ids` array. The session waits indefinitely for a response. 3. Send a `user.tool_confirmation` event for each blocking event, passing the event ID in the `tool_use_id` parameter. Set `result` to `"allow"` or `"deny"`. Use `deny_message` to explain a denial. You can send several confirmations in a single `events` request. 4. Once all blocking events are resolved, the session transitions back to `running`. Allowed tools execute. Denied tools do not run, and the agent receives a tool result saying the call was rejected, including your `deny_message`. In the following examples, the tool-use event IDs come from the `stop_reason.event_ids` array of the `session.status_idle` event. Learn more about receiving events in the [Session event stream](https://platform.claude.com/docs/en/managed-agents/events-and-streaming#integrating-events) guide, or [subscribe to webhooks](https://platform.claude.com/docs/en/managed-agents/webhooks) to be notified when a session pauses for input. ```bash cURL # Allow the tool to execute curl -fsSL "https://api.anthropic.com/v1/sessions/$SESSION_ID/events" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d '{ "events": [ { "type": "user.tool_confirmation", "tool_use_id": "'$AGENT_TOOL_USE_EVENT_ID'", "result": "allow" } ] }' # Or deny it with an explanation curl -fsSL "https://api.anthropic.com/v1/sessions/$SESSION_ID/events" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d '{ "events": [ { "type": "user.tool_confirmation", "tool_use_id": "'$MCP_TOOL_USE_EVENT_ID'", "result": "deny", "deny_message": "Don'\''t create issues in the production project. Use the staging project." } ] }' ``` ```bash CLI # Allow the tool to execute ant beta:sessions:events send \ --session-id "$SESSION_ID" \ --event "{type: user.tool_confirmation, tool_use_id: $AGENT_TOOL_USE_EVENT_ID, result: allow}" # Or deny it with an explanation ant beta:sessions:events send \ --session-id "$SESSION_ID" \ --event "{type: user.tool_confirmation, tool_use_id: $MCP_TOOL_USE_EVENT_ID, result: deny, deny_message: Don't create issues in the production project. Use the staging project.}" ``` ```python Python # Allow the tool to execute client.beta.sessions.events.send( session.id, events=[ { "type": "user.tool_confirmation", "tool_use_id": agent_tool_use_event.id, "result": "allow", }, ], ) # Or deny it with an explanation client.beta.sessions.events.send( session.id, events=[ { "type": "user.tool_confirmation", "tool_use_id": mcp_tool_use_event.id, "result": "deny", "deny_message": "Don't create issues in the production project. Use the staging project.", }, ], ) ``` ```typescript TypeScript // Allow the tool to execute await client.beta.sessions.events.send(session.id, { events: [ { type: "user.tool_confirmation", tool_use_id: agent_tool_use_event.id, result: "allow" } ] }); // Or deny it with an explanation await client.beta.sessions.events.send(session.id, { events: [ { type: "user.tool_confirmation", tool_use_id: mcp_tool_use_event.id, result: "deny", deny_message: "Don't create issues in the production project. Use the staging project." } ] }); ``` ```csharp C# // Allow the tool to execute await client.Beta.Sessions.Events.Send(session.ID, new() { Events = [ new BetaManagedAgentsUserToolConfirmationEventParams { Type = BetaManagedAgentsUserToolConfirmationEventParamsType.UserToolConfirmation, ToolUseID = agentToolUseEvent.ID, Result = "allow", }, ], }); // Or deny it with an explanation await client.Beta.Sessions.Events.Send(session.ID, new() { Events = [ new BetaManagedAgentsUserToolConfirmationEventParams { Type = BetaManagedAgentsUserToolConfirmationEventParamsType.UserToolConfirmation, ToolUseID = mcpToolUseEvent.ID, Result = "deny", DenyMessage = "Don't create issues in the production project. Use the staging project.", }, ], }); ``` ```go Go // Allow the tool to execute _, err = client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{ Events: []anthropic.BetaManagedAgentsEventParamsUnion{{ OfUserToolConfirmation: &anthropic.BetaManagedAgentsUserToolConfirmationEventParams{ Type: anthropic.BetaManagedAgentsUserToolConfirmationEventParamsTypeUserToolConfirmation, ToolUseID: agentToolUseEvent.ID, Result: anthropic.BetaManagedAgentsUserToolConfirmationEventParamsResultAllow, }, }}, }) if err != nil { panic(err) } // Or deny it with an explanation _, err = client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{ Events: []anthropic.BetaManagedAgentsEventParamsUnion{{ OfUserToolConfirmation: &anthropic.BetaManagedAgentsUserToolConfirmationEventParams{ Type: anthropic.BetaManagedAgentsUserToolConfirmationEventParamsTypeUserToolConfirmation, ToolUseID: mcpToolUseEvent.ID, Result: anthropic.BetaManagedAgentsUserToolConfirmationEventParamsResultDeny, DenyMessage: anthropic.String("Don't create issues in the production project. Use the staging project."), }, }}, }) if err != nil { panic(err) } ``` ```java Java // Allow the tool to execute client.beta().sessions().events().send( session.id(), EventSendParams.builder() .addEvent( BetaManagedAgentsUserToolConfirmationEventParams.builder() .type(BetaManagedAgentsUserToolConfirmationEventParams.Type.USER_TOOL_CONFIRMATION) .toolUseId(agentToolUseEvent.id()) .result(BetaManagedAgentsUserToolConfirmationEventParams.Result.ALLOW) .build() ) .build() ); // Or deny it with an explanation client.beta().sessions().events().send( session.id(), EventSendParams.builder() .addEvent( BetaManagedAgentsUserToolConfirmationEventParams.builder() .type(BetaManagedAgentsUserToolConfirmationEventParams.Type.USER_TOOL_CONFIRMATION) .toolUseId(mcpToolUseEvent.id()) .result(BetaManagedAgentsUserToolConfirmationEventParams.Result.DENY) .denyMessage("Don't create issues in the production project. Use the staging project.") .build() ) .build() ); ``` ```php PHP use Anthropic\Beta\Sessions\Events\ManagedAgentsUserToolConfirmationEventParams; // Allow the tool to execute $client->beta->sessions->events->send( $session->id, events: [ ManagedAgentsUserToolConfirmationEventParams::with( type: 'user.tool_confirmation', toolUseID: $agentToolUseEvent->id, result: 'allow', ), ], ); // Or deny it with an explanation $client->beta->sessions->events->send( $session->id, events: [ ManagedAgentsUserToolConfirmationEventParams::with( type: 'user.tool_confirmation', toolUseID: $mcpToolUseEvent->id, result: 'deny', denyMessage: "Don't create issues in the production project. Use the staging project.", ), ], ); ``` ```ruby Ruby # Allow the tool to execute client.beta.sessions.events.send_( session.id, events: [ { type: "user.tool_confirmation", tool_use_id: agent_tool_use_event.id, result: "allow" } ] ) # Or deny it with an explanation client.beta.sessions.events.send_( session.id, events: [ { type: "user.tool_confirmation", tool_use_id: mcp_tool_use_event.id, result: "deny", deny_message: "Don't create issues in the production project. Use the staging project." } ] ) ``` ## Custom tools Permission policies do not apply to custom tools. When the agent invokes a custom tool, your application receives an `agent.custom_tool_use` event and is responsible for deciding whether to execute it before sending back a `user.custom_tool_result`. See [Session event stream](https://platform.claude.com/docs/en/managed-agents/events-and-streaming#handling-custom-tool-calls) for the full flow. ## Next steps Attach reusable, filesystem-based expertise to your agent for domain-specific workflows. Send events, stream responses, and interrupt or redirect your session mid-execution. --- title: Skills url: https://platform.claude.com/docs/en/managed-agents/skills description: Attach reusable, filesystem-based expertise to your agent for domain-specific workflows. --- Skills are reusable, filesystem-based resources that give your agent domain-specific expertise: workflows, context, and best practices that turn a general-purpose agent into a specialist. Each skill you add incurs a modest cost on the session's context window, adding instructions and metadata that help the model use the skill. Learn more in the [Agent Skills](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview) overview. Skills reach your agent in two ways: attach them through the agent's `skills` array, or [load them from a GitHub repository](https://platform.claude.com/docs/en/managed-agents/skills#load-skills-from-a-github-repository) mounted on the session. Attached skills come in two types. All skills work the same way: your agent invokes them automatically when they are relevant to the task. * **Pre-built Anthropic skills:** Common document tasks such as PowerPoint, Excel, Word, and PDF handling (`pptx`, `xlsx`, `docx`, `pdf`). * **Custom skills:** Skills you author and upload to your workspace. To learn how to author custom skills, see [Agent Skills](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview) and [Skill authoring best practices](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices). To upload a custom skill to your workspace, see [Create a custom skill](https://platform.claude.com/docs/en/managed-agents/skills#create-a-custom-skill). Managed Agents API requests require the `managed-agents-2026-04-01` beta header, except memory store endpoints, which use `agent-memory-2026-07-22` instead. The SDK sets the correct beta header automatically. See [Beta headers](https://platform.claude.com/docs/en/api/beta-headers#endpoint-specific-headers). ## Create a custom skill A custom skill is a directory containing a `SKILL.md` file plus any supporting files, uploaded to your workspace as a zip archive or as individual files. Creating the skill returns the `skill_*` ID you reference when attaching it to an agent. Anthropic pre-built skills are already available in every workspace and don't require this step. To use only pre-built skills, skip to [Attach skills to an agent](https://platform.claude.com/docs/en/managed-agents/skills#attach-skills-to-an-agent). When you call the Skills API directly with cURL, pass the `anthropic-beta: skills-2025-10-02` header explicitly. The CLI and SDKs send it automatically. These examples omit the optional `display_title` field, so the skill's title is derived from `SKILL.md`. An explicitly passed `display_title` must be unique among the custom skills in your workspace. ```bash cURL curl -X POST "https://api.anthropic.com/v1/skills" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: skills-2025-10-02" \ -F "files[]=@example_skill.zip" ``` ```bash CLI ant beta:skills create \ --file example_skill.zip ``` ```python Python import anthropic from anthropic.lib import files_from_dir client = anthropic.Anthropic() skill = client.beta.skills.create( files=files_from_dir("example_skill"), ) print(f"Created skill: {skill.id}") print(f"Latest version: {skill.latest_version}") ``` ```typescript TypeScript import Anthropic from "@anthropic-ai/sdk"; import { toFile } from "@anthropic-ai/sdk"; import fs from "node:fs"; const client = new Anthropic(); const skill = await client.beta.skills.create({ files: [await toFile(fs.createReadStream("example_skill.zip"), "example_skill.zip")] }); console.log(`Created skill: ${skill.id}`); console.log(`Latest version: ${skill.latest_version}`); ``` ```csharp C# using System.IO; using Anthropic; using Anthropic.Models.Beta.Skills; AnthropicClient client = new(); var parameters = new SkillCreateParams { Files = [ new FileStream("example_skill.zip", FileMode.Open, FileAccess.Read) ], }; var skill = await client.Beta.Skills.Create(parameters); Console.WriteLine($"Created skill: {skill.ID}"); Console.WriteLine($"Latest version: {skill.LatestVersion}"); ``` ```go Go package main import ( "context" "fmt" "io" "log" "os" "github.com/anthropics/anthropic-sdk-go" ) func main() { client := anthropic.NewClient() zipFile, err := os.Open("example_skill.zip") if err != nil { log.Fatal(err) } defer zipFile.Close() skill, err := client.Beta.Skills.New(context.TODO(), anthropic.BetaSkillNewParams{ Files: []io.Reader{zipFile}, }) if err != nil { log.Fatal(err) } fmt.Printf("Created skill: %s\n", skill.ID) fmt.Printf("Latest version: %s\n", skill.LatestVersion) } ``` ```java Java import com.anthropic.client.AnthropicClient; import com.anthropic.client.okhttp.AnthropicOkHttpClient; import com.anthropic.core.MultipartField; import com.anthropic.models.beta.skills.SkillCreateParams; import com.anthropic.models.beta.skills.SkillCreateResponse; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; void main() throws IOException { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); SkillCreateParams params = SkillCreateParams.builder() .addFile(MultipartField.builder() .value(Files.newInputStream(Path.of("example_skill.zip"))) .filename("example_skill.zip") .contentType("application/zip") .build()) .build(); SkillCreateResponse skill = client.beta().skills().create(params); IO.println("Created skill: " + skill.id()); IO.println("Latest version: " + skill.latestVersion().orElseThrow()); } ``` ```php PHP beta->skills->create( files: [ FileParam::fromResource(fopen('example_skill.zip', 'r')) ], ); echo "Created skill: {$skill->id}\n"; echo "Latest version: {$skill->latestVersion}\n"; ``` ```ruby Ruby require "anthropic" client = Anthropic::Client.new skill = client.beta.skills.create( files: [ File.open("example_skill.zip", "rb") ] ) puts "Created skill: #{skill.id}" puts "Latest version: #{skill.latest_version}" ``` To list, retrieve, delete, and version custom skills, see [Managing custom skills](https://platform.claude.com/docs/en/build-with-claude/skills-guide#managing-custom-skills). For the full request and response schemas, see the [Create Skill API reference](https://platform.claude.com/docs/en/api/beta/skills/create). Skill bundles upload directly to the Skills API rather than through the [Files API](https://platform.claude.com/docs/en/build-with-claude/files). ## Attach skills to an agent Attach skills when creating an agent. Each [session](https://platform.claude.com/docs/en/managed-agents/sessions) supports up to 500 skills, counted as the deduplicated set across every agent in the session (see [Multiagent orchestration](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration)). Mounting more skills increases the time it takes for the session's sandbox to start. Attach only the skills each agent needs for its task. Each entry in the `skills` array uses the following fields: | Field | Description | | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `type` | Either `anthropic` for pre-built skills or `custom` for workspace-authored skills. | | `skill_id` | The skill identifier. For Anthropic skills, use the short name (for example, `xlsx`). For custom skills, use the `skill_*` ID returned at creation (see [Create a custom skill](https://platform.claude.com/docs/en/managed-agents/skills#create-a-custom-skill)). | | `version` | Pin to a specific version or use `latest`. Optional. Defaults to `latest` when omitted. Applies to both Anthropic and custom skills. | ```bash cURL agent=$(curl -sS https://api.anthropic.com/v1/agents \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ --json @- <<'EOF' { "name": "Financial Analyst", "model": "claude-opus-5", "system": "You are a financial analysis agent.", "skills": [ {"type": "anthropic", "skill_id": "xlsx"}, {"type": "custom", "skill_id": "skill_01AbCdEfGhIjKlMnOpQrStUv", "version": "latest"} ] } EOF ) ``` ```bash CLI ant beta:agents create <<'YAML' name: Financial Analyst model: claude-opus-5 system: You are a financial analysis agent. skills: - type: anthropic skill_id: xlsx - type: custom skill_id: skill_01AbCdEfGhIjKlMnOpQrStUv version: latest YAML ``` ```python Python agent = client.beta.agents.create( name="Financial Analyst", model="claude-opus-5", system="You are a financial analysis agent.", skills=[ { "type": "anthropic", "skill_id": "xlsx", }, { "type": "custom", "skill_id": "skill_01AbCdEfGhIjKlMnOpQrStUv", "version": "latest", }, ], ) ``` ```typescript TypeScript const agent = await client.beta.agents.create({ name: "Financial Analyst", model: "claude-opus-5", system: "You are a financial analysis agent.", skills: [ { type: "anthropic", skill_id: "xlsx" }, { type: "custom", skill_id: "skill_01AbCdEfGhIjKlMnOpQrStUv", version: "latest" } ] }); ``` ```csharp C# using Anthropic.Models.Beta.Agents; var agent = await client.Beta.Agents.Create(new() { Name = "Financial Analyst", Model = BetaManagedAgentsModel.ClaudeOpus5, System = "You are a financial analysis agent.", Skills = [ new BetaManagedAgentsAnthropicSkillParams { Type = BetaManagedAgentsAnthropicSkillParamsType.Anthropic, SkillID = "xlsx" }, new BetaManagedAgentsCustomSkillParams { Type = BetaManagedAgentsCustomSkillParamsType.Custom, SkillID = "skill_01AbCdEfGhIjKlMnOpQrStUv", Version = "latest" }, ], }); ``` ```go Go agent, err := client.Beta.Agents.New(ctx, anthropic.BetaAgentNewParams{ Name: "Financial Analyst", Model: anthropic.BetaManagedAgentsModelConfigParams{ ID: anthropic.BetaManagedAgentsModelClaudeOpus5, }, System: anthropic.String("You are a financial analysis agent."), Skills: []anthropic.BetaManagedAgentsSkillParamsUnion{ {OfAnthropic: &anthropic.BetaManagedAgentsAnthropicSkillParams{ SkillID: "xlsx", Type: anthropic.BetaManagedAgentsAnthropicSkillParamsTypeAnthropic, }}, {OfCustom: &anthropic.BetaManagedAgentsCustomSkillParams{ SkillID: "skill_01AbCdEfGhIjKlMnOpQrStUv", Type: anthropic.BetaManagedAgentsCustomSkillParamsTypeCustom, Version: anthropic.String("latest"), }}, }, }) if err != nil { panic(err) } _ = agent ``` ```java Java import com.anthropic.models.beta.agents.*; var agent = client.beta().agents().create( AgentCreateParams.builder() .name("Financial Analyst") .model(BetaManagedAgentsModel.CLAUDE_OPUS_5) .system("You are a financial analysis agent.") .addSkill( BetaManagedAgentsAnthropicSkillParams.builder() .type(BetaManagedAgentsAnthropicSkillParams.Type.ANTHROPIC) .skillId("xlsx") .build() ) .addSkill( BetaManagedAgentsCustomSkillParams.builder() .type(BetaManagedAgentsCustomSkillParams.Type.CUSTOM) .skillId("skill_01AbCdEfGhIjKlMnOpQrStUv") .version("latest") .build() ) .build() ); ``` ```php PHP $agent = $client->beta->agents->create( name: 'Financial Analyst', model: 'claude-opus-5', system: 'You are a financial analysis agent.', skills: [ ['type' => 'anthropic', 'skill_id' => 'xlsx'], ['type' => 'custom', 'skill_id' => 'skill_01AbCdEfGhIjKlMnOpQrStUv', 'version' => 'latest'], ], ); ``` ```ruby Ruby agent = client.beta.agents.create( name: "Financial Analyst", model: "claude-opus-5", system_: "You are a financial analysis agent.", skills: [ {type: "anthropic", skill_id: "xlsx"}, {type: "custom", skill_id: "skill_01AbCdEfGhIjKlMnOpQrStUv", version: "latest"} ] ) ``` ## Load skills from a GitHub repository Skills can also live in your codebase. When a session mounts a repository through the [`github_repository` resource](https://platform.claude.com/docs/en/managed-agents/github), the repository's root `.claude/skills` directory is scanned at session start, and each skill found there becomes available to the agent. No upload and no entry in the agent's `skills` array are required. The agent sees each discovered skill's name, description, and path in the sandbox, and reads the skill's `SKILL.md` when a task matches, including any scripts and resources the skill ships. Discovery relies on the agent's `read` tool from the [agent toolset](https://platform.claude.com/docs/en/managed-agents/tools), which is enabled by default; an agent with `read` disabled doesn't load repository skills. Repository skills are agent instructions, so a mounted repository is part of your agent's trust boundary. Anyone who can commit to the repository (a merged external pull request, a compromised dependency, a contributor) can add or change a skill, the platform loads it at session start without a review step, and session tools such as `bash` and `web_fetch` give those instructions real reach. Mount only repositories you trust, and review `.claude/skills` before mounting a repository that accepts outside contributions. Repository skill discovery runs in cloud sandboxes. [Self-hosted sandboxes](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes) don't support GitHub repository resources. Discovery finds skills at exactly `.claude/skills//SKILL.md`, one directory level deep at the repository root: * `your-repo/` * `.claude/` * `skills/` * `code-review/` * `SKILL.md` * `release-process/` * `SKILL.md` * `scripts/` * `run_checks.sh` * `src/` Locations that don't match this layout aren't discovered at session start: * `.claude/skills/SKILL.md`: a `SKILL.md` with no skill directory around it * `.claude/skills/tools/code-review/SKILL.md`: nested more than one directory level deep * `skills/code-review/SKILL.md`: a `skills` directory outside `.claude` A `.claude/skills` directory elsewhere in the repository, such as inside a package subdirectory, isn't announced at session start; those skills can still surface when the agent reads files under that subtree. Repository skills use the same `SKILL.md` format as the custom skills you upload. For the format and authoring guidance, see [Agent Skills](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview) and [Skill authoring best practices](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices). To load skills from a repository, create a session that mounts it. This is the same request shown in [Accessing GitHub](https://platform.claude.com/docs/en/managed-agents/github#token-permissions); `mount_path` is optional and defaults to `/workspace/`: ```bash cURL session_id=$(curl -fsS https://api.anthropic.com/v1/sessions \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ --data @- <beta->sessions->create( agent: $agent->id, environmentID: $environment->id, resources: [ [ 'type' => 'github_repository', 'url' => 'https://github.com/org/repo', 'mountPath' => '/workspace/repo', 'authorizationToken' => 'ghp_your_github_token', ], ], ); ``` ```ruby Ruby session = client.beta.sessions.create( agent: agent.id, environment_id: environment.id, resources: [ { type: "github_repository", url: "https://github.com/org/repo", mount_path: "/workspace/repo", authorization_token: "ghp_your_github_token" } ] ) ``` For private repositories, the resource's `authorization_token` must have access to the repository. This is the same personal access token flow used for any repository mount; see [Accessing GitHub](https://platform.claude.com/docs/en/managed-agents/github#token-permissions). Discovered skills follow the checked-out state of the repository: the `checkout` branch or commit when the resource sets one, otherwise the repository's default branch. The scan runs once, when the session starts. Commits pushed mid-session are not picked up; to load updated skills, start a new session. Repository skills work alongside skills attached through the agent's `skills` array. If a repository skill shares a name with an attached skill, or with a skill from another mounted repository, both are available; each is announced with its own path. ## Next steps Customize cloud sandboxes for your sessions. Learn how to use Agent Skills to extend Claude's capabilities through the API. Upload files once and reference them across API requests. Learn how to use Agent Skills to create documents with the Claude API in under 10 minutes. --- title: Tools url: https://platform.claude.com/docs/en/managed-agents/tools description: Configure tools available to your agent. --- Claude Managed Agents provides a set of built-in tools that Claude can use autonomously within a [session](https://platform.claude.com/docs/en/managed-agents/sessions). You control which tools are available by specifying them in the agent configuration. Claude Managed Agents also supports custom, user-defined tools. Your application executes these tools separately and returns the results to Claude, which uses them to continue the task. To give the agent tools from an MCP server, use the [MCP connector](https://platform.claude.com/docs/en/managed-agents/mcp-connector) instead. Managed Agents API requests require the `managed-agents-2026-04-01` beta header, except memory store endpoints, which use `agent-memory-2026-07-22` instead. The SDK sets the correct beta header automatically. See [Beta headers](https://platform.claude.com/docs/en/api/beta-headers#endpoint-specific-headers). ## Available tools The agent toolset includes the following tools. All are enabled by default when you include the toolset in your agent configuration. Use the values in the Name column to reference tools in the `configs` array. | Tool | Name | Description | | ---------- | ------------ | ---------------------------------------------- | | Bash | `bash` | Execute bash commands in a shell session | | Read | `read` | Read a file from the sandbox filesystem | | Write | `write` | Write a file to the sandbox filesystem | | Edit | `edit` | Perform string replacement in a file | | Glob | `glob` | Fast file pattern matching using glob patterns | | Grep | `grep` | Text search using regex patterns | | Web fetch | `web_fetch` | Fetch content from a URL | | Web search | `web_search` | Search the web for information | When a tool output exceeds 100,000 characters (about 25,000 tokens), it is automatically written to a file in the [sandbox](https://platform.claude.com/docs/en/managed-agents/environments). The model receives a truncated preview with the file path and can read the full content from there. ## Configuring the toolset Enable the full toolset with `agent_toolset_20260401` when creating an agent. Use the `configs` array to disable specific tools or override their settings. Each config entry can also set a `permission_policy` that controls whether the tool's calls are auto-approved or require confirmation. See [Permission policies](https://platform.claude.com/docs/en/managed-agents/permission-policies) for the available policy types. ```bash cURL agent=$(curl -fsSL https://api.anthropic.com/v1/agents \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d @- <<'EOF' { "name": "Coding Assistant", "model": "claude-opus-5", "tools": [ { "type": "agent_toolset_20260401", "configs": [ {"name": "web_fetch", "enabled": false} ] } ] } EOF ) ``` ```bash CLI ant beta:agents create <<'YAML' name: Coding Assistant model: claude-opus-5 tools: - type: agent_toolset_20260401 configs: - name: web_fetch enabled: false YAML ``` ```python Python agent = client.beta.agents.create( name="Coding Assistant", model="claude-opus-5", tools=[ { "type": "agent_toolset_20260401", "configs": [ {"name": "web_fetch", "enabled": False}, ], }, ], ) ``` ```typescript TypeScript const agent = await client.beta.agents.create({ name: "Coding Assistant", model: "claude-opus-5", tools: [ { type: "agent_toolset_20260401", configs: [{ name: "web_fetch", enabled: false }] } ] }); ``` ```csharp C# using Anthropic.Models.Beta.Agents; var agent = await client.Beta.Agents.Create(new() { Name = "Coding Assistant", Model = new("claude-opus-5"), Tools = [ new BetaManagedAgentsAgentToolset20260401Params { Type = "agent_toolset_20260401", Configs = [ new() { Name = "web_fetch", Enabled = false }, ], }, ], }); ``` ```go Go agent, err := client.Beta.Agents.New(ctx, anthropic.BetaAgentNewParams{ Name: "Coding Assistant", Model: anthropic.BetaManagedAgentsModelConfigParams{ ID: "claude-opus-5", }, Tools: []anthropic.BetaAgentNewParamsToolUnion{{ OfAgentToolset20260401: &anthropic.BetaManagedAgentsAgentToolset20260401Params{ Type: anthropic.BetaManagedAgentsAgentToolset20260401ParamsTypeAgentToolset20260401, Configs: []anthropic.BetaManagedAgentsAgentToolConfigParams{{ Name: anthropic.BetaManagedAgentsAgentToolConfigParamsNameWebFetch, Enabled: anthropic.Bool(false), }}, }, }}, }) if err != nil { panic(err) } _ = agent ``` ```java Java import com.anthropic.models.beta.agents.*; var agent = client.beta().agents().create(AgentCreateParams.builder() .name("Coding Assistant") .model(BetaManagedAgentsModel.CLAUDE_OPUS_5) .addTool(BetaManagedAgentsAgentToolset20260401Params.builder() .type(BetaManagedAgentsAgentToolset20260401Params.Type.AGENT_TOOLSET_20260401) .addConfig(BetaManagedAgentsAgentToolConfigParams.builder() .name(BetaManagedAgentsAgentToolConfigParams.Name.WEB_FETCH) .enabled(false) .build()) .build()) .build()); ``` ```php PHP use Anthropic\Beta\Agents\BetaManagedAgentsAgentToolConfigParams; use Anthropic\Beta\Agents\BetaManagedAgentsAgentToolset20260401Params; $agent = $client->beta->agents->create( name: 'Coding Assistant', model: 'claude-opus-5', tools: [ BetaManagedAgentsAgentToolset20260401Params::with( type: 'agent_toolset_20260401', configs: [ BetaManagedAgentsAgentToolConfigParams::with(name: 'web_fetch', enabled: false), ], ), ], ); ``` ```ruby Ruby agent = client.beta.agents.create( name: "Coding Assistant", model: "claude-opus-5", tools: [ { type: :agent_toolset_20260401, configs: [ {name: :web_fetch, enabled: false} ] } ] ) ``` ### Disabling specific tools To disable a tool, set `enabled: false` in its config entry in the toolset object of your agent's `tools` array: ```json { "type": "agent_toolset_20260401", "configs": [ { "name": "web_fetch", "enabled": false }, { "name": "web_search", "enabled": false } ] } ``` ### Enabling only specific tools The `default_config` object sets the baseline for every tool in the set, and per-tool `configs` entries override it. To start with everything off and enable only what you need, set `default_config.enabled` to `false`: ```json { "type": "agent_toolset_20260401", "default_config": { "enabled": false }, "configs": [ { "name": "bash", "enabled": true }, { "name": "read", "enabled": true }, { "name": "write", "enabled": true } ] } ``` ## Custom tools In addition to built-in tools, you can define custom tools. Custom tools are analogous to [user-defined client tools](https://platform.claude.com/docs/en/agents-and-tools/tool-use/how-tool-use-works#user-defined-tools-client-executed) in the Messages API. Each custom tool defines a contract: you specify what operations are available and what they return, and Claude determines when and how to call them. The model never executes anything on its own. It emits a structured request, your code runs the operation, and the result flows back into the conversation. See [Session event stream](https://platform.claude.com/docs/en/managed-agents/events-and-streaming#handling-custom-tool-calls) for how to receive custom tool calls and return results during a session. If your sessions run in a self-hosted sandbox, the environment worker can [serve custom tools from your sandbox](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#serve-custom-tools-from-your-sandbox), including tools that wrap an MCP server inside your network. ```bash cURL agent=$(curl -fsSL https://api.anthropic.com/v1/agents \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d @- <<'EOF' { "name": "Weather Agent", "model": "claude-opus-5", "tools": [ { "type": "agent_toolset_20260401" }, { "type": "custom", "name": "get_weather", "description": "Get current weather for a location", "input_schema": { "type": "object", "properties": { "location": {"type": "string", "description": "City name"} }, "required": ["location"] } } ] } EOF ) ``` ```bash CLI ant beta:agents create <<'YAML' name: Weather Agent model: claude-opus-5 tools: - type: agent_toolset_20260401 - type: custom name: get_weather description: Get current weather for a location input_schema: type: object properties: location: type: string description: City name required: - location YAML ``` ```python Python agent = client.beta.agents.create( name="Weather Agent", model="claude-opus-5", tools=[ { "type": "agent_toolset_20260401", }, { "type": "custom", "name": "get_weather", "description": "Get current weather for a location", "input_schema": { "type": "object", "properties": { "location": {"type": "string", "description": "City name"}, }, "required": ["location"], }, }, ], ) ``` ```typescript TypeScript const agent = await client.beta.agents.create({ name: "Weather Agent", model: "claude-opus-5", tools: [ { type: "agent_toolset_20260401" }, { type: "custom", name: "get_weather", description: "Get current weather for a location", input_schema: { type: "object", properties: { location: { type: "string", description: "City name" } }, required: ["location"] } } ] }); ``` ```csharp C# using System.Text.Json; using Anthropic.Models.Beta.Agents; var agent = await client.Beta.Agents.Create(new() { Name = "Weather Agent", Model = new("claude-opus-5"), Tools = [ new BetaManagedAgentsAgentToolset20260401Params { Type = "agent_toolset_20260401", }, new BetaManagedAgentsCustomToolParams { Type = "custom", Name = "get_weather", Description = "Get current weather for a location", InputSchema = new() { Properties = new Dictionary { ["location"] = JsonSerializer.SerializeToElement( new { type = "string", description = "City name" } ), }, Required = ["location"], }, }, ], }); ``` ```go Go agent, err := client.Beta.Agents.New(ctx, anthropic.BetaAgentNewParams{ Name: "Weather Agent", Model: anthropic.BetaManagedAgentsModelConfigParams{ ID: "claude-opus-5", }, Tools: []anthropic.BetaAgentNewParamsToolUnion{{ OfAgentToolset20260401: &anthropic.BetaManagedAgentsAgentToolset20260401Params{ Type: anthropic.BetaManagedAgentsAgentToolset20260401ParamsTypeAgentToolset20260401, }, }, { OfCustom: &anthropic.BetaManagedAgentsCustomToolParams{ Type: anthropic.BetaManagedAgentsCustomToolParamsTypeCustom, Name: "get_weather", Description: "Get current weather for a location", InputSchema: anthropic.BetaManagedAgentsCustomToolInputSchemaParam{ Properties: map[string]any{ "location": map[string]any{ "type": "string", "description": "City name", }, }, Required: []string{"location"}, }, }, }}, }) if err != nil { panic(err) } _ = agent ``` ```java Java import com.anthropic.models.beta.agents.*; import java.util.Map; var agent = client.beta().agents().create(AgentCreateParams.builder() .name("Weather Agent") .model(BetaManagedAgentsModel.CLAUDE_OPUS_5) .addTool(BetaManagedAgentsAgentToolset20260401Params.builder() .type(BetaManagedAgentsAgentToolset20260401Params.Type.AGENT_TOOLSET_20260401) .build()) .addTool(BetaManagedAgentsCustomToolParams.builder() .type(BetaManagedAgentsCustomToolParams.Type.CUSTOM) .name("get_weather") .description("Get current weather for a location") .inputSchema(BetaManagedAgentsCustomToolInputSchema.builder() .properties(BetaManagedAgentsCustomToolInputSchema.Properties.builder() .putAdditionalProperty("location", JsonValue.from(Map.of( "type", "string", "description", "City name"))) .build()) .addRequired("location") .build()) .build()) .build()); ``` ```php PHP use Anthropic\Beta\Agents\BetaManagedAgentsAgentToolset20260401Params; use Anthropic\Beta\Agents\BetaManagedAgentsCustomToolInputSchema; use Anthropic\Beta\Agents\BetaManagedAgentsCustomToolParams; $agent = $client->beta->agents->create( name: 'Weather Agent', model: 'claude-opus-5', tools: [ BetaManagedAgentsAgentToolset20260401Params::with( type: 'agent_toolset_20260401', ), BetaManagedAgentsCustomToolParams::with( type: 'custom', name: 'get_weather', description: 'Get current weather for a location', inputSchema: BetaManagedAgentsCustomToolInputSchema::with( properties: ['location' => ['type' => 'string', 'description' => 'City name']], required: ['location'], ), ), ], ); ``` ```ruby Ruby agent = client.beta.agents.create( name: "Weather Agent", model: "claude-opus-5", tools: [ {type: :agent_toolset_20260401}, { type: :custom, name: "get_weather", description: "Get current weather for a location", input_schema: { type: :object, properties: {location: {type: "string", description: "City name"}}, required: ["location"] } } ] ) ``` Once you've defined custom tools on the agent, the agent invokes them during a session. ### Best practices for custom tool definitions * **Provide extremely detailed descriptions.** This is by far the most important factor in tool performance. Your descriptions should explain what the tool does and when to use it (and when not to). Explain what each parameter means and how it affects the tool's behavior. Call out any important caveats or limitations. The more context you can give Claude about your tools, the better it is at determining when and how to use them. Aim for three to four sentences for each tool description, more if the tool is complex. * **Consolidate related operations into fewer tools.** Rather than creating a separate tool for every action (`create_pr`, `review_pr`, `merge_pr`), group them into a single tool with an `action` parameter. Fewer, more capable tools reduce selection ambiguity and make your tool surface easier for Claude to navigate. * **Use meaningful namespacing in tool names.** When your tools span multiple services or resources, prefix names with the resource (for example, `db_query` or `storage_read`). This makes tool selection unambiguous as your library grows. * **Design tool responses to return only high-signal information.** Return semantic, stable identifiers (for example, slugs or UUIDs) rather than opaque internal references, and include only the fields Claude needs to determine its next step. Bloated responses waste context and make it harder for Claude to extract what matters. ## Next steps Connect MCP servers to your agents for access to external tools and data sources. Control when agent and MCP tools execute. Send events, stream responses, and interrupt or redirect your session mid-execution. ### Configure agent environment --- title: Cloud environment setup url: https://platform.claude.com/docs/en/managed-agents/environments description: Customize cloud sandboxes for your sessions. --- Environments define the sandbox configuration where your agent runs. You create an environment once, then reference its ID each time you start a session. Multiple sessions can share the same environment, but each session gets its own isolated sandbox (a fresh Linux container). This page covers `type: cloud` environments. To run sandboxes on your own infrastructure, see [Self-hosted sandboxes](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes). Managed Agents API requests require the `managed-agents-2026-04-01` beta header, except memory store endpoints, which use `agent-memory-2026-07-22` instead. The SDK sets the correct beta header automatically. See [Beta headers](https://platform.claude.com/docs/en/api/beta-headers#endpoint-specific-headers). ## Create an environment ```bash cURL environment=$(curl -fsS https://api.anthropic.com/v1/environments \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ --data @- <<'EOF' { "name": "python-dev", "config": { "type": "cloud", "networking": {"type": "unrestricted"} } } EOF ) environment_id=$(jq -r '.id' <<< "$environment") echo "Environment ID: $environment_id" ``` ```bash CLI ant beta:environments create \ --name "python-dev" \ --config '{type: cloud, networking: {type: unrestricted}}' ``` ```python Python environment = client.beta.environments.create( name="python-dev", config={ "type": "cloud", "networking": {"type": "unrestricted"}, }, ) print(f"Environment ID: {environment.id}") ``` ```typescript TypeScript const environment = await client.beta.environments.create({ name: "python-dev", config: { type: "cloud", networking: { type: "unrestricted" }, }, }); console.log(`Environment ID: ${environment.id}`); ``` ```csharp C# var environment = await client.Beta.Environments.Create(new() { Name = "python-dev", Config = new BetaCloudConfigParams { Networking = new BetaUnrestrictedNetwork(), }, }); Console.WriteLine($"Environment ID: {environment.ID}"); ``` ```go Go environment, err := client.Beta.Environments.New(ctx, anthropic.BetaEnvironmentNewParams{ Name: "python-dev", Config: anthropic.BetaEnvironmentNewParamsConfigUnion{ OfCloud: &anthropic.BetaCloudConfigParams{ Networking: anthropic.BetaCloudConfigParamsNetworkingUnion{ OfUnrestricted: &anthropic.BetaUnrestrictedNetworkParam{}, }, }, }, }) if err != nil { panic(err) } fmt.Printf("Environment ID: %s\n", environment.ID) ``` ```java Java var environment = client.beta().environments().create(EnvironmentCreateParams.builder() .name("python-dev") .config(BetaCloudConfigParams.builder() .networking(BetaUnrestrictedNetwork.builder().build()) .build()) .build()); IO.println("Environment ID: " + environment.id()); ``` ```php PHP $environment = $client->beta->environments->create( name: 'python-dev', config: ['type' => 'cloud', 'networking' => ['type' => 'unrestricted']], ); echo "Environment ID: {$environment->id}\n"; ``` ```ruby Ruby environment = client.beta.environments.create( name: "python-dev", config: { type: "cloud", networking: {type: "unrestricted"} } ) puts "Environment ID: #{environment.id}" ``` Use a unique, descriptive `name` so you can tell environments apart. ## Use the environment in a session Pass the environment ID as a string when [creating a session](https://platform.claude.com/docs/en/managed-agents/sessions). ```bash cURL session=$(curl -fsS https://api.anthropic.com/v1/sessions \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ --data @- <beta->sessions->create( agent: $agent->id, environmentID: $environment->id, ); ``` ```ruby Ruby session = client.beta.sessions.create( agent: agent.id, environment_id: environment.id ) ``` ## Configuration options ### Packages The `packages` field pre-installs packages into the sandbox before the agent starts. Packages are installed by their respective package managers and cached across sessions that share the same environment. When multiple package managers are specified, they run in alphabetical order (apt, cargo, gem, go, npm, pip). You can optionally pin specific versions. Unpinned packages install the latest version. ```bash cURL environment=$(curl -fsS https://api.anthropic.com/v1/environments \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ --data @- <<'EOF' { "name": "data-analysis", "config": { "type": "cloud", "packages": { "pip": ["pandas", "numpy", "scikit-learn"], "npm": ["express"] }, "networking": {"type": "unrestricted"} } } EOF ) ``` ```bash CLI ant beta:environments create <<'YAML' name: data-analysis config: type: cloud packages: pip: - pandas - numpy - scikit-learn npm: - express networking: type: unrestricted YAML ``` ```python Python environment = client.beta.environments.create( name="data-analysis", config={ "type": "cloud", "packages": { "pip": ["pandas", "numpy", "scikit-learn"], "npm": ["express"], }, "networking": {"type": "unrestricted"}, }, ) ``` ```typescript TypeScript const environment = await client.beta.environments.create({ name: "data-analysis", config: { type: "cloud", packages: { pip: ["pandas", "numpy", "scikit-learn"], npm: ["express"] }, networking: { type: "unrestricted" } } }); ``` ```csharp C# using Anthropic.Models.Beta.Environments; var environment = await client.Beta.Environments.Create(new() { Name = "data-analysis", Config = new BetaCloudConfigParams { Packages = new() { Pip = ["pandas", "numpy", "scikit-learn"], Npm = ["express"], }, Networking = new BetaUnrestrictedNetwork(), }, }); ``` ```go Go environment, err := client.Beta.Environments.New(ctx, anthropic.BetaEnvironmentNewParams{ Name: "data-analysis", Config: anthropic.BetaEnvironmentNewParamsConfigUnion{ OfCloud: &anthropic.BetaCloudConfigParams{ Packages: anthropic.BetaPackagesParams{ Pip: []string{"pandas", "numpy", "scikit-learn"}, Npm: []string{"express"}, }, Networking: anthropic.BetaCloudConfigParamsNetworkingUnion{ OfUnrestricted: &anthropic.BetaUnrestrictedNetworkParam{}, }, }, }, }) if err != nil { panic(err) } _ = environment ``` ```java Java import com.anthropic.models.beta.environments.*; import java.util.List; var environment = client.beta().environments().create(EnvironmentCreateParams.builder() .name("data-analysis") .config(BetaCloudConfigParams.builder() .packages(BetaPackagesParams.builder() .pip(List.of("pandas", "numpy", "scikit-learn")) .npm(List.of("express")) .build()) .networking(BetaUnrestrictedNetwork.builder().build()) .build()) .build()); ``` ```php PHP $environment = $client->beta->environments->create( name: 'data-analysis', config: [ 'type' => 'cloud', 'packages' => [ 'pip' => ['pandas', 'numpy', 'scikit-learn'], 'npm' => ['express'], ], 'networking' => ['type' => 'unrestricted'], ], ); ``` ```ruby Ruby environment = client.beta.environments.create( name: "data-analysis", config: { type: "cloud", packages: { pip: %w[pandas numpy scikit-learn], npm: %w[express] }, networking: {type: "unrestricted"} } ) ``` Supported package managers: | Field | Package manager | Example | | ------- | ------------------------- | ------------------------------------------- | | `apt` | System packages (apt-get) | `"ffmpeg"` | | `cargo` | Rust (cargo) | `"ripgrep@14.0.0"` | | `gem` | Ruby (gem) | `"rails:7.1.0"` | | `go` | Go modules | `"golang.org/x/tools/cmd/goimports@latest"` | | `npm` | Node.js (npm) | `"express@4.18.0"` | | `pip` | Python (pip) | `"pandas==2.2.0"` | ### Networking The `networking` field controls the sandbox's outbound network access. It does not affect the allowed domains for the `web_search` or `web_fetch` tools. | Mode | Description | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `unrestricted` | Full outbound network access, except for a general safety blocklist. This is the default. | | `limited` | Restricts sandbox network access to the hosts in `allowed_hosts`. Set `allow_package_managers` and `allow_mcp_servers` to `true` to allow additional access. | The following example creates an environment with `limited` networking: ```bash cURL curl -fsS https://api.anthropic.com/v1/environments \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d '{ "name": "api-access", "config": { "type": "cloud", "networking": { "type": "limited", "allowed_hosts": ["api.example.com"], "allow_mcp_servers": true, "allow_package_managers": true } } }' ``` ```bash CLI ant beta:environments create <<'YAML' name: api-access config: type: cloud networking: type: limited allowed_hosts: - api.example.com allow_mcp_servers: true allow_package_managers: true YAML ``` ```python Python environment = client.beta.environments.create( name="api-access", config={ "type": "cloud", "networking": { "type": "limited", "allowed_hosts": ["api.example.com"], "allow_mcp_servers": True, "allow_package_managers": True, }, }, ) ``` ```typescript TypeScript const environment = await client.beta.environments.create({ name: "api-access", config: { type: "cloud", networking: { type: "limited", allowed_hosts: ["api.example.com"], allow_mcp_servers: true, allow_package_managers: true } } }); ``` ```csharp C# using Anthropic.Models.Beta.Environments; var environment = await client.Beta.Environments.Create(new() { Name = "api-access", Config = new BetaCloudConfigParams { Networking = new BetaLimitedNetworkParams { AllowedHosts = ["api.example.com"], AllowMcpServers = true, AllowPackageManagers = true, }, }, }); ``` ```go Go environment, err := client.Beta.Environments.New(ctx, anthropic.BetaEnvironmentNewParams{ Name: "api-access", Config: anthropic.BetaEnvironmentNewParamsConfigUnion{ OfCloud: &anthropic.BetaCloudConfigParams{ Networking: anthropic.BetaCloudConfigParamsNetworkingUnion{ OfLimited: &anthropic.BetaLimitedNetworkParams{ AllowedHosts: []string{"api.example.com"}, AllowMCPServers: anthropic.Bool(true), AllowPackageManagers: anthropic.Bool(true), }, }, }, }, }) if err != nil { panic(err) } _ = environment ``` ```java Java import com.anthropic.models.beta.environments.*; import java.util.List; var environment = client.beta().environments().create(EnvironmentCreateParams.builder() .name("api-access") .config(BetaCloudConfigParams.builder() .networking(BetaLimitedNetworkParams.builder() .allowedHosts(List.of("api.example.com")) .allowMcpServers(true) .allowPackageManagers(true) .build()) .build()) .build()); ``` ```php PHP $environment = $client->beta->environments->create( name: 'api-access', config: [ 'type' => 'cloud', 'networking' => [ 'type' => 'limited', 'allowed_hosts' => ['api.example.com'], 'allow_mcp_servers' => true, 'allow_package_managers' => true, ], ], ); ``` ```ruby Ruby environment = client.beta.environments.create( name: "api-access", config: { type: "cloud", networking: { type: "limited", allowed_hosts: %w[api.example.com], allow_mcp_servers: true, allow_package_managers: true } } ) ``` For production deployments, use `limited` networking with an explicit `allowed_hosts` list. Follow the principle of least privilege by granting only the minimum network access your agent requires, and regularly audit your allowed domains. When using `limited` networking: * `allowed_hosts` specifies domains the sandbox can reach. Specify bare hostnames or wildcard patterns (such as `*.example.com`). Do not include a URL scheme, port, or path. * `allow_mcp_servers` allows outbound access to MCP server endpoints configured on the agent, beyond those listed in the `allowed_hosts` array. Defaults to `false`. * `allow_package_managers` allows outbound access to public package registries (such as PyPI and npm) beyond those listed in the `allowed_hosts` array. Defaults to `false`. ## Environment lifecycle * Environments persist until explicitly archived or deleted. * Each session gets its own sandbox instance, even when multiple sessions reference the same environment. Sessions do not share filesystem state. * Environments are not versioned. If you update an environment frequently, keep your own record of the changes so you can tell which configuration each session used. ## Manage environments ```bash cURL # List environments environments=$(curl -fsS https://api.anthropic.com/v1/environments \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01") # Retrieve a specific environment env=$(curl -fsS "https://api.anthropic.com/v1/environments/$environment_id" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01") # Archive an environment (read-only, existing sessions continue) curl -fsS -X POST "https://api.anthropic.com/v1/environments/$environment_id/archive" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" # Delete an environment (only if no sessions reference it) curl -fsS -X DELETE "https://api.anthropic.com/v1/environments/$environment_id" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" ``` ```bash CLI # List environments ant beta:environments list # Retrieve a specific environment ant beta:environments retrieve --environment-id "$ENVIRONMENT_ID" # Archive an environment (read-only, existing sessions continue) ant beta:environments archive --environment-id "$ENVIRONMENT_ID" # Delete an environment (only if no sessions reference it) ant beta:environments delete --environment-id "$ENVIRONMENT_ID" ``` ```python Python # List environments environments = client.beta.environments.list() # Retrieve a specific environment env = client.beta.environments.retrieve(environment.id) # Archive an environment (read-only, existing sessions continue) client.beta.environments.archive(environment.id) # Delete an environment (only if no sessions reference it) client.beta.environments.delete(environment.id) ``` ```typescript TypeScript // List environments const environments = await client.beta.environments.list(); // Retrieve a specific environment const env = await client.beta.environments.retrieve(environment.id); // Archive an environment (read-only, existing sessions continue) await client.beta.environments.archive(environment.id); // Delete an environment (only if no sessions reference it) await client.beta.environments.delete(environment.id); ``` ```csharp C# // List environments var environments = await client.Beta.Environments.List(); // Retrieve a specific environment var env = await client.Beta.Environments.Retrieve(environment.ID); // Archive an environment (read-only, existing sessions continue) await client.Beta.Environments.Archive(environment.ID); // Delete an environment (only if no sessions reference it) await client.Beta.Environments.Delete(environment.ID); ``` ```go Go // List environments environments, err := client.Beta.Environments.List(ctx, anthropic.BetaEnvironmentListParams{}) // ... // Retrieve a specific environment env, err := client.Beta.Environments.Get(ctx, environment.ID, anthropic.BetaEnvironmentGetParams{}) // ... // Archive an environment (read-only, existing sessions continue) _, err = client.Beta.Environments.Archive(ctx, environment.ID, anthropic.BetaEnvironmentArchiveParams{}) // ... // Delete an environment (only if no sessions reference it) _, err = client.Beta.Environments.Delete(ctx, environment.ID, anthropic.BetaEnvironmentDeleteParams{}) ``` ```java Java // List environments var environments = client.beta().environments().list(); // Retrieve a specific environment var env = client.beta().environments().retrieve(environment.id()); // Archive an environment (read-only, existing sessions continue) client.beta().environments().archive(environment.id()); // Delete an environment (only if no sessions reference it) client.beta().environments().delete(environment.id()); ``` ```php PHP // List environments $environments = $client->beta->environments->list(); // Retrieve a specific environment $env = $client->beta->environments->retrieve($environment->id); // Archive an environment (read-only, existing sessions continue) $client->beta->environments->archive($environment->id); // Delete an environment (only if no sessions reference it) $client->beta->environments->delete($environment->id); ``` ```ruby Ruby # List environments environments = client.beta.environments.list # Retrieve a specific environment env = client.beta.environments.retrieve(environment.id) # Archive an environment (read-only, existing sessions continue) client.beta.environments.archive(environment.id) # Delete an environment (only if no sessions reference it) client.beta.environments.delete(environment.id) ``` ## Pre-installed runtimes Cloud sandboxes include common runtimes out of the box. See [Cloud sandbox reference](https://platform.claude.com/docs/en/managed-agents/cloud-sandboxes-reference) for the full list of pre-installed languages, databases, and utilities. ## Next steps Pre-installed packages, databases, and utilities available in cloud sandboxes. Create a session to run your agent and start running tasks. --- title: Cloud sandbox reference url: https://platform.claude.com/docs/en/managed-agents/cloud-sandboxes-reference description: Pre-installed packages, databases, and utilities available in cloud sandboxes. --- Cloud sandboxes run as isolated Linux containers on Anthropic-managed infrastructure. They come pre-installed with a comprehensive set of programming languages, databases, and utilities. The agent can use these immediately without any installation steps. These specifications apply to `cloud` environments. Self-hosted sandboxes run on your infrastructure with whatever your worker provides. Managed Agents API requests require the `managed-agents-2026-04-01` beta header, except memory store endpoints, which use `agent-memory-2026-07-22` instead. The SDK sets the correct beta header automatically. See [Beta headers](https://platform.claude.com/docs/en/api/beta-headers#endpoint-specific-headers). ## Programming languages | Language | Version | Package manager | | -------- | ------- | --------------- | | Python | 3.12+ | pip, uv | | Node.js | 20+ | npm, yarn, pnpm | | Go | 1.22+ | go modules | | Rust | 1.77+ | cargo | | Java | 21+ | maven, gradle | | Ruby | 3.3+ | bundler, gem | | PHP | 8.3+ | composer | | C/C++ | GCC 13+ | make, cmake | ## Databases | Database | Description | | ----------------- | -------------------------------------------------- | | SQLite | Pre-installed, available immediately | | PostgreSQL client | `psql` client for connecting to external databases | | Redis client | `redis-cli` for connecting to external instances | Database servers (such as PostgreSQL and Redis) are not running in the sandbox by default. The sandbox includes client tools for connecting to external database instances. SQLite is fully available for local use. ## Utilities ### System tools * `git` - Version control * `curl`, `wget` - HTTP clients * `jq` - JSON processing * `tar`, `zip`, `unzip` - Archive tools * `ssh`, `scp` - Remote access (requires a networking mode that allows the destination host) * `tmux`, `screen` - Terminal multiplexers ### Development tools * `make`, `cmake` - Build systems * `docker` - Container management (limited availability) * `ripgrep` (`rg`) - Fast file search * `tree` - Directory visualization * `htop` - Process monitoring ### Text processing * `sed`, `awk`, `grep` - Stream editors * `vim`, `nano` - Text editors * `diff`, `patch` - File comparison ## Sandbox specifications | Property | Value | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Operating system | Ubuntu 22.04 LTS | | Architecture | x86\_64 (amd64) | | Memory | Up to 8 GB | | Disk space | Up to 10 GB | | Network | API-created environments default to [`unrestricted` networking](https://platform.claude.com/docs/en/managed-agents/environments#networking); sandboxes provisioned through Claude Studio default to `limited` | ### Configure agent environment > Self-hosted sandboxes --- title: Security model url: https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes-security description: Shared responsibility model for self-hosted sandbox environments. --- Anthropic secures the control plane across all environments: session and work queue integrity, multitenant isolation, and agent-context minimization. When you self-host, the following responsibilities fall to you. ## What you own * **Sandbox image quality and runtime hardening.** Anthropic does not inspect or verify your sandbox image. Follow best practices such as dropping unnecessary Linux capabilities, running as a non-root user, and using a read-only root filesystem. * **Network egress controls.** Your sandbox's network access is determined by your VPC and firewall rules. Without egress restrictions, a compromised tool execution can reach arbitrary external hosts. Restrict outbound traffic to only the endpoints your tools require. * **Service key storage and rotation.** The environment service key (`ANTHROPIC_ENVIRONMENT_KEY`) authorizes polling your environment's work queue and submitting results back to sessions. Store it in a secrets manager, not in environment files or sandbox images. Rotate it immediately if you suspect exposure. * **Isolating untrusted workloads.** The environment service key is scoped to one environment's work queue. If you run untrusted code inside your sandbox, consider provisioning a separate workspace and environment for each trust boundary. This limits each key to a single user's sessions instead of a shared pool. * **Tool-execution blast radius.** Tools run inside your sandbox with whatever permissions your process has. Apply least privilege to the process user and mount only the directories your tools require. * **Log retention and session content.** Conversation content and tool outputs pass through your worker and stay in your environment. You are responsible for retaining, redacting, or deleting that data in compliance with your own policies. Anthropic has no visibility into what your worker does with session content once delivered. ## What Anthropic cannot do for you * **Know that your key leaked.** Anthropic can detect anomalous usage patterns, but cannot know your key was compromised. If you suspect `ANTHROPIC_ENVIRONMENT_KEY` leaked, revoke it and generate a replacement immediately. Revocation is validated on every request, so it takes effect on the worker's next call. * **Verify your worker build.** Anthropic does not inspect your sandbox image or runtime. A supply-chain compromise in your image is not detectable from the control plane. * **Isolate tools inside your sandbox.** Anthropic's security boundary stops at the sandbox. How you isolate individual tool executions from each other inside that boundary is entirely your responsibility. * **Enforce data retention in your environment.** Once session content reaches your worker, it is outside Anthropic's data lifecycle controls. --- title: Self-hosted sandboxes url: https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes description: Run Claude Managed Agents sessions in self-hosted sandboxes, keeping tool execution, files, and network egress in your own infrastructure. --- By default, Managed Agents executes tools and code inside [Anthropic-managed cloud sandboxes](https://platform.claude.com/docs/en/managed-agents/cloud-sandboxes-reference). Self-hosted sandboxes keep the orchestration on Anthropic's side but move tool execution into infrastructure you control, so the agent's code, filesystem, and network egress never leave your environment. Tool execution stays on your host: the filesystem the agent reads and writes, the processes it spawns, and the network it can reach are all under your control. Tool inputs and outputs still flow to Anthropic's control plane (where Claude runs) so the model can see results and determine what to do next. See the [security model](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes-security) for the full data-flow boundary. Self-hosted sandboxes support all Claude models available in Managed Agents, including Claude Opus 4.8 and Claude Opus 5. The model is configured on the [agent](https://platform.claude.com/docs/en/managed-agents/agent-setup), not the environment. ## How it differs from cloud environments | | Cloud environment | Self-hosted sandbox | | ----------------------------- | --------------------------- | ------------------- | | Where tools run | Anthropic-managed sandboxes | Your infrastructure | | Network reach | Anthropic's egress controls | Your network policy | | File and GitHub repo mounting | Managed by Anthropic | Managed by you | | Lifecycle | Managed by Anthropic | Managed by you | Self-hosting is a good fit when the agent needs to operate on data that cannot leave your network boundary, reach internal services that are not publicly routable, or run under your organization's own compliance and audit controls. For Zero Data Retention and HIPAA BAA eligibility, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#feature-eligibility). ## When to combine with MCP tunnels Self-hosting controls *where the agent's code executes*. [MCP tunnels](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/overview) control *how Anthropic reaches MCP servers in your network*. They are independent: a session running in Anthropic's cloud sandboxes can still reach private MCP servers through a tunnel, and a self-hosted session can use either tunneled or public MCP servers. Use both when you want execution and tool access to stay inside your boundary. To give the agent tools from an MCP server inside your network without running a tunnel, you can also [wrap the server as custom tools](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#wrap-an-mcp-server-as-custom-tools) served by your worker. ## Environment worker This guide describes how to build a worker with any generic sandboxing platform. Additional, platform-specific guides are available for [AWS Lambda MicroVMs](https://docs.aws.amazon.com/lambda/latest/dg/microvms-integrations-claude-managed-agents.html), [Blaxel](https://docs.blaxel.ai/Tutorials/Claude-Managed-Agents), [Cloudflare](https://developers.cloudflare.com/sandbox/claude-managed-agents/), [Daytona](https://www.daytona.io/docs/en/guides/claude/claude-managed-agents), [E2B](https://e2b.dev/docs/agents/claude-managed-agents), [Fly.io](https://docs.sprites.dev/integrations/claude-managed-agents/), [GKE Agent Sandbox](https://github.com/GoogleCloudPlatform/kubernetes-engine-samples/tree/main/ai-ml/anthropic-agent-sandbox), [Modal](https://github.com/modal-labs/claude-managed-agents-modal-sandbox), [Namespace](https://namespace.so/docs/integrations/claude), [Superserve](https://docs.superserve.ai/integrations/managed-agents/claude-managed-agents), and [Vercel](https://vercel.com/kb/guide/run-claude-managed-agent-tools-with-vercel-sandbox). An environment worker is a process you run on your own infrastructure. It receives tool execution requests from Anthropic and runs them locally. The `self_hosted` environment acts as a work queue: when a [session](https://platform.claude.com/docs/en/managed-agents/sessions) is assigned to it, Anthropic enqueues the session as a work item. Your worker claims work items from that queue, spawns an execution context for each one, downloads the agent's [skills](https://platform.claude.com/docs/en/managed-agents/skills) (reusable, filesystem-based resources that give the agent domain-specific expertise), runs the tool calls, and posts the results back. Work items are claimed by polling the environment's queue: either by an **always-on worker** that polls continuously, or a **webhook-triggered handler** that wakes on `session.status_run_started` and starts polling. The CLI and SDK both ship pre-built workers. The `ant` CLI supports the always-on pattern only; the SDK supports both always-on and webhook-triggered. Both are configurable: see [Self-hosted worker](https://platform.claude.com/docs/en/managed-agents/reference#self-hosted-worker) in the reference for CLI flags, and [SDK helpers](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#sdk-helpers) on this page for the SDK options. For more control, call the [Environments Work endpoints](https://platform.claude.com/docs/en/api/beta/environments/work) directly and implement your own worker. ### Sandbox filesystem * **`/workspace`:** the system default working directory for tool execution and skill download. The CLI's `--workdir` flag defaults to the current directory; pass `--workdir /workspace` to match the system default. Skills are downloaded to `/skills//`. If you use a different working directory, update your agent's system prompt so Claude can locate the skill files. * **Outputs:** on self-hosted environments the session's system prompt omits the `/mnt/session/outputs` instruction used on Anthropic-managed sandboxes, so final deliverables land wherever the agent writes them in your sandbox filesystem, typically under the working directory. ## Before you begin You need: * **An existing agent.** If you don't have one, complete the [Quickstart](https://platform.claude.com/docs/en/managed-agents/quickstart) first and note its agent ID. * **A Linux host** with `/bin/bash` at that exact path. The worker's bash tool invokes it directly, without consulting `PATH`. The TypeScript SDK additionally requires `unzip` and `tar` on the `PATH` and Node.js 22 or later; the Python and Go SDKs use their standard libraries for archive extraction and have no additional binary requirements. * **The `ant` CLI or an Anthropic SDK** (Python, TypeScript, or Go) on the worker host. * **Two credentials:** an environment key (generated in the Console in the steps that follow) authenticates the worker to its queue; your Claude API key creates sessions and reads queue stats from outside the worker host. Key generation is Console-only. On [Claude Platform on AWS](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws), the worker authenticates with AWS IAM (SigV4) or an [API key generated in the AWS Console](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#api-key-authentication), not an environment key. Attach the [`AnthropicSelfHostedEnvironmentAccess`](https://platform.claude.com/docs/en/api/claude-platform-on-aws-iam-actions#managed-policies) managed policy to the IAM principal your worker runs as. Environment keys generated in the Claude Console don't work with the Claude Platform on AWS endpoint. In the [Console](https://platform.claude.com/workspaces/default/environments): **Workspace > Environments > New > Self-hosted** Or through the API: ```bash cURL curl -sS --fail-with-body https://api.anthropic.com/v1/environments \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d '{ "name": "self-hosted", "config": {"type": "self_hosted"} }' ``` ```bash CLI ant beta:environments create \ --name self-hosted \ --config '{"type": "self_hosted"}' ``` ```python Python client = anthropic.Anthropic() environment = client.beta.environments.create( name="self-hosted", config={"type": "self_hosted"} ) print(environment.id) ``` ```typescript TypeScript const client = new Anthropic(); const environment = await client.beta.environments.create({ name: "self-hosted", config: { type: "self_hosted" } }); console.log(environment.id); ``` ```csharp C# using Anthropic.Models.Beta.Environments; var client = new AnthropicClient(); var environment = await client.Beta.Environments.Create( new EnvironmentCreateParams { Name = "self-hosted", Config = new BetaSelfHostedConfigParams(), } ); Console.WriteLine(environment.ID); ``` ```go Go client := anthropic.NewClient() environment, err := client.Beta.Environments.New(context.Background(), anthropic.BetaEnvironmentNewParams{ Name: "self-hosted", Config: anthropic.BetaEnvironmentNewParamsConfigUnion{ OfSelfHosted: &anthropic.BetaSelfHostedConfigParams{}, }, }) if err != nil { panic(err) } fmt.Println(environment.ID) ``` ```java Java import com.anthropic.models.beta.environments.BetaSelfHostedConfigParams; import com.anthropic.models.beta.environments.EnvironmentCreateParams; void main() { var client = AnthropicOkHttpClient.fromEnv(); var environment = client.beta().environments().create( EnvironmentCreateParams.builder() .name("self-hosted") .config(BetaSelfHostedConfigParams.builder().build()) .build() ); IO.println(environment.id()); } ``` ```php PHP $client = new Anthropic\Client(); $environment = $client->beta->environments->create( name: 'self-hosted', config: ['type' => 'self_hosted'], ); echo $environment->id, PHP_EOL; ``` ```ruby Ruby client = Anthropic::Client.new environment = client.beta.environments.create( name: "self-hosted", config: {type: :self_hosted} ) puts environment.id ``` In the Console, open the environment and click **Generate environment key**. Key generation is Console-only, regardless of whether you created the environment through the Console or the API. Then export the environment ID and key on the worker host: ```bash export ANTHROPIC_ENVIRONMENT_KEY="sk-ant-oat01-..." export ANTHROPIC_ENVIRONMENT_ID="env_..." ``` Skills can include executables that the agent may run directly. The CLI and SDK workers preserve the executable permissions recorded in the skill bundle when they extract it. If you implement skills download manually, you are responsible for setting executable permissions. ## Run a worker Choose **always-on** for the simplest setup: a long-running process polls the queue continuously and needs only outbound HTTPS. Choose **webhook-triggered** to avoid running an idle poller; it requires a webhook endpoint that Anthropic can reach (see [Webhooks](https://platform.claude.com/docs/en/managed-agents/webhooks) for endpoint setup and signature verification). Run this on the worker host. For Linux environments, download the release binary directly. ```bash VERSION=1.22.1 OS=$(uname -s | tr '[:upper:]' '[:lower:]') case $(uname -m) in x86_64) ARCH=amd64 ;; aarch64) ARCH=arm64 ;; esac curl -fsSL "https://github.com/anthropics/anthropic-cli/releases/download/v${VERSION}/ant_${VERSION}_${OS}_${ARCH}.tar.gz" \ | sudo tar -xz -C /usr/local/bin ant ``` You can find all releases on the [GitHub releases page](https://github.com/anthropics/anthropic-cli/releases). ```bash brew install anthropics/tap/ant ``` **In-process** `ant beta:worker poll` claims work items assigned to the environment, downloads skills, executes tool calls in the working directory, and posts results back. It reads `ANTHROPIC_ENVIRONMENT_KEY` and `ANTHROPIC_ENVIRONMENT_ID` from the environment. ```bash ant beta:worker poll \ --workdir "/workspace" ``` The worker exits cleanly on SIGTERM or SIGINT: it cancels any in-flight tool call, posts its error result, and releases the work item before stopping. **Sandbox per session** If you need stronger isolation (a fresh filesystem, resource limits, or per-session network controls), run each session in its own sandbox. Build an image with `ant` installed and `ant beta:worker run` as the entrypoint. The base image must provide `/bin/bash`; `curl` is only used at build time. When a sandbox starts, it reads session details from environment variables, handles that session, and exits: ```text FROM your-base-image ARG ANT_VERSION=1.22.1 ARG TARGETARCH RUN ARCH=$([ "$TARGETARCH" = "arm64" ] && echo arm64 || echo amd64) && \ curl -fsSL "https://github.com/anthropics/anthropic-cli/releases/download/v${ANT_VERSION}/ant_${ANT_VERSION}_linux_${ARCH}.tar.gz" \ | tar -xz -C /usr/local/bin ant WORKDIR /workspace VOLUME /workspace ENTRYPOINT ["ant", "beta:worker", "run"] ``` Then write a spawn script that forwards session details into a fresh sandbox. The poller injects `ANTHROPIC_SESSION_ID`, `ANTHROPIC_WORK_ID`, `ANTHROPIC_ENVIRONMENT_ID`, and `ANTHROPIC_ENVIRONMENT_KEY` into the script's environment. `ANTHROPIC_BASE_URL` is optional and is passed through only if it was set on the poller host; it overrides the default API endpoint. In the example, `/host/outputs` is a host directory you choose; it is bind-mounted to the sandbox's working directory (`/workspace`) so you can retrieve session deliverables after the sandbox exits. On self-hosted environments the agent writes deliverables under the working directory rather than `/mnt/session/outputs` (see [Sandbox filesystem](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#sandbox-filesystem)), so mounting the working directory is what captures them; the mount also picks up the downloaded `skills/` tree and any intermediate files the agent creates. ```bash #!/bin/bash # spawn.sh: called once per claimed work item mkdir -p "/host/outputs/$ANTHROPIC_SESSION_ID" exec docker run --rm \ -e ANTHROPIC_SESSION_ID -e ANTHROPIC_ENVIRONMENT_KEY \ -e ANTHROPIC_WORK_ID -e ANTHROPIC_ENVIRONMENT_ID -e ANTHROPIC_BASE_URL \ -v "/host/outputs/$ANTHROPIC_SESSION_ID":/workspace \ your-image ``` Start the poller pointing at the script: ```bash ant beta:worker poll \ --on-work ./spawn.sh ``` `EnvironmentWorker` claims work items assigned to the environment, downloads skills, executes tool calls in the working directory, and posts results back. Authenticate with the environment key you generated in [Before you begin](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#before-you-begin). ```python Python import asyncio import os from anthropic import AsyncAnthropic from anthropic.lib.environments import EnvironmentWorker async def main() -> None: environment_key = os.environ["ANTHROPIC_ENVIRONMENT_KEY"] environment_id = os.environ["ANTHROPIC_ENVIRONMENT_ID"] async with AsyncAnthropic(auth_token=environment_key) as client: await EnvironmentWorker( client, environment_id=environment_id, environment_key=environment_key, workdir="/workspace", ).run() asyncio.run(main()) ``` ```typescript TypeScript import Anthropic from "@anthropic-ai/sdk"; import { EnvironmentWorker } from "@anthropic-ai/sdk/helpers/beta/environments"; const environmentKey = process.env.ANTHROPIC_ENVIRONMENT_KEY!; const environmentId = process.env.ANTHROPIC_ENVIRONMENT_ID!; const client = new Anthropic({ authToken: environmentKey }); const controller = new AbortController(); process.once("SIGTERM", () => controller.abort()); await new EnvironmentWorker({ client, environmentId, environmentKey, workdir: "/workspace", signal: controller.signal }).run(); ``` ```csharp C# // EnvironmentWorker is not currently available in the C# SDK. See the Always-on (ant CLI) tab. ``` ```go Go package main import ( "context" "log" "os" "os/signal" "syscall" "github.com/anthropics/anthropic-sdk-go" "github.com/anthropics/anthropic-sdk-go/lib/environments" "github.com/anthropics/anthropic-sdk-go/option" ) func main() { environmentKey := os.Getenv("ANTHROPIC_ENVIRONMENT_KEY") environmentID := os.Getenv("ANTHROPIC_ENVIRONMENT_ID") ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() client := anthropic.NewClient(option.WithAuthToken(environmentKey)) worker := environments.NewEnvironmentWorker(client, environments.EnvironmentWorkerOptions{ EnvironmentID: environmentID, EnvironmentKey: environmentKey, Workdir: "/workspace", }) if err := worker.Run(ctx); err != nil { log.Fatalf("worker: %v", err) } } ``` ```java Java // EnvironmentWorker is not currently available in the Java SDK. See the Always-on (ant CLI) tab. ``` ```php PHP // EnvironmentWorker is not currently available in the PHP SDK. See the Always-on (ant CLI) tab. ``` ```ruby Ruby # EnvironmentWorker is not currently available in the Ruby SDK. See the Always-on (ant CLI) tab. ``` In the [Console](https://platform.claude.com/settings/workspaces/default/webhooks), define a webhook endpoint that listens for `session.status_run_started` events. See [Webhooks](https://platform.claude.com/docs/en/managed-agents/webhooks) for details. In addition to the environment ID and key from [Before you begin](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#before-you-begin), export the webhook signing key on your handler host so the handler can verify incoming payloads. Signature verification in the Python handler needs the webhooks extra: `pip install "anthropic[webhooks]"`. ```bash export ANTHROPIC_WEBHOOK_SIGNING_KEY="whsec_..." ``` `EnvironmentWorker` claims the work item, downloads skills, executes tool calls in the working directory, posts results back, and exits. Invoke it when `session.status_run_started` fires. ```python Python import os import anthropic environment_key = os.environ["ANTHROPIC_ENVIRONMENT_KEY"] environment_id = os.environ["ANTHROPIC_ENVIRONMENT_ID"] client = anthropic.AsyncAnthropic( auth_token=environment_key, ) async def handle(raw: bytes, headers: dict[str, str]) -> dict: event = client.beta.webhooks.unwrap(raw.decode(), headers=headers) if event.data.type != "session.status_run_started": return {"status": "ignored"} async for work in client.beta.environments.work.poller( environment_id=environment_id, environment_key=environment_key, block_ms=None, reclaim_older_than_ms=2000, drain=True, auto_stop=False, ): await client.beta.environments.work.worker(workdir="/workspace").handle_item( work_id=work.id, environment_id=environment_id, session_id=work.data.id, environment_key=environment_key, ) return {"status": "ok"} ``` ```typescript TypeScript import Anthropic from "@anthropic-ai/sdk"; const environmentKey = process.env.ANTHROPIC_ENVIRONMENT_KEY!; const environmentId = process.env.ANTHROPIC_ENVIRONMENT_ID!; const client = new Anthropic({ authToken: environmentKey }); export async function handle(req: Request): Promise { const body = await req.text(); let event; try { event = client.beta.webhooks.unwrap(body, { headers: Object.fromEntries(req.headers) }); } catch { return new Response("signature verification failed", { status: 401 }); } if (event.data.type !== "session.status_run_started") { return Response.json({ status: "ignored" }); } for await (const work of client.beta.environments.work.poller({ environmentId, environmentKey, blockMs: null, reclaimOlderThanMs: 2000, drain: true, autoStop: false })) { await client.beta.environments.work.worker({ workdir: "/workspace" }).handleItem({ workId: work.id, environmentId, sessionId: work.data.id, environmentKey }); } return Response.json({ status: "ok" }); } ``` ```csharp C# // EnvironmentWorker is not currently available in the C# SDK. // To handle work items directly, see the Environments Work endpoints. ``` ```go Go package main import ( "context" "encoding/json" "io" "log/slog" "net/http" "os" "github.com/anthropics/anthropic-sdk-go" "github.com/anthropics/anthropic-sdk-go/lib/environments" "github.com/anthropics/anthropic-sdk-go/option" "github.com/anthropics/anthropic-sdk-go/packages/param" ) var ( environmentKey = os.Getenv("ANTHROPIC_ENVIRONMENT_KEY") environmentID = os.Getenv("ANTHROPIC_ENVIRONMENT_ID") client = anthropic.NewClient( option.WithAuthToken(environmentKey), option.WithWebhookKey(os.Getenv("ANTHROPIC_WEBHOOK_SIGNING_KEY")), ) worker = environments.NewEnvironmentWorker(client, environments.EnvironmentWorkerOptions{ Workdir: "/workspace", }) ) func handle(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) if err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } event, err := client.Beta.Webhooks.Unwrap(body, r.Header) if err != nil { http.Error(w, "signature verification failed", http.StatusUnauthorized) return } if event.Data.Type != "session.status_run_started" { json.NewEncoder(w).Encode(map[string]string{"status": "ignored"}) return } // The Go SDK does not provide a RunOne convenience: drain pending items // with WorkPoller and run each one with HandleItem. // Detach from r.Context(): the session can outlive the webhook delivery timeout. ctx := context.Background() poller := environments.NewWorkPoller(ctx, client, environments.WorkPollerOptions{ EnvironmentID: environmentID, EnvironmentKey: environmentKey, BlockMs: param.Null[int64](), ReclaimOlderThanMs: param.NewOpt[int64](2000), Drain: true, }) defer poller.Close() for poller.Next() { item := poller.Current() if err := worker.HandleItem(ctx, environments.HandleItemOptions{ WorkID: item.ID, EnvironmentID: item.EnvironmentID, SessionID: item.Data.ID, EnvironmentKey: environmentKey, }); err != nil { slog.Error("handle work item", "work_id", item.ID, "err", err) http.Error(w, "internal error", http.StatusInternalServerError) return } } if err := poller.Err(); err != nil { slog.Error("poll work queue", "err", err) http.Error(w, "internal error", http.StatusInternalServerError) return } json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) } func main() { http.HandleFunc("POST /webhook", handle) if err := http.ListenAndServe(":8080", nil); err != nil { slog.Error("http server", "err", err) os.Exit(1) } } ``` ```java Java // EnvironmentWorker is not currently available in the Java SDK. // To handle work items directly, see the Environments Work endpoints. ``` ```php PHP // EnvironmentWorker is not currently available in the PHP SDK. // To handle work items directly, see the Environments Work endpoints. ``` ```ruby Ruby # EnvironmentWorker is not currently available in the Ruby SDK. # To handle work items directly, see the Environments Work endpoints. ``` ### SDK helpers The SDK provides three helpers at different levels of control. `EnvironmentWorker` covers most use cases; drop to the lower-level helpers when you need to launch your own per-session process or run tools against an already-claimed session. * **`EnvironmentWorker`:** the out-of-the-box worker. Handles polling, setup, and execution end to end. * `.run()`: runs indefinitely, picking up sessions as they arrive. * `.handle_item()`: handles a single claimed work item and exits. Pass the work, session, and environment identifiers explicitly, or let it read the `ANTHROPIC_*` variables that `ant beta:worker poll --on-work` sets for the process it spawns. * **`work.poller()`:** polls the work queue on your behalf and gives you each claimed session. Use this when you want to decide what happens for each session, for example launching a sandbox rather than running tools in-process. * `drain`: whether to stop polling once the queue is empty rather than waiting for new work. * `block_ms`: how long to wait for work to arrive before returning, in milliseconds. Must be between 1 and 999 (per-poll wait; the helper re-polls automatically). Pass `null` (`None` in Python, `param.Null[int64]()` in Go) for a non-blocking check; omitting the parameter uses the default 999 ms long-poll. * `reclaim_older_than_ms`: re-claim work items that were claimed but never acknowledged within this many milliseconds. * `auto_stop`: whether to post a stop signal for each work item once your loop body finishes with it. The Go poller has no opt-out and always posts the stop signal, so block in the loop body until the session completes rather than detaching. * **`client.beta.sessions.events.tool_runner()`:** runs tool calls for a single session, given the session ID and a tool list. Use when you've already claimed the work and only need the execution layer. Use the work poller directly when you want to launch your own per-session process, for example spinning up a sandbox for each claimed session: ```bash cURL # The work poller is an SDK helper (Python, TypeScript, Go), not a raw # endpoint. From the shell, use `ant beta:worker poll --on-work` instead; # see the Always-on (ant CLI) tab. ``` ```bash CLI # The work poller is an SDK helper (Python, TypeScript, Go), not a raw # endpoint. From the shell, use `ant beta:worker poll --on-work` instead; # see the Always-on (ant CLI) tab. ``` ```python Python import asyncio import os from anthropic import AsyncAnthropic from anthropic.types.beta.environments import BetaSelfHostedWork async def launch_container(work: BetaSelfHostedWork) -> None: # Replace with your own per-session sandbox launcher. Pass # ANTHROPIC_ENVIRONMENT_KEY into the launched sandbox, never # your API key. print(f"claimed session {work.data.id}") async def main() -> None: environment_key = os.environ["ANTHROPIC_ENVIRONMENT_KEY"] environment_id = os.environ["ANTHROPIC_ENVIRONMENT_ID"] async with AsyncAnthropic(auth_token=environment_key) as client: async for work in client.beta.environments.work.poller( environment_id=environment_id, environment_key=environment_key, auto_stop=False, # the launched sandbox owns the stop call ): await launch_container(work) asyncio.run(main()) ``` ```typescript TypeScript import Anthropic from "@anthropic-ai/sdk"; import { WorkPoller } from "@anthropic-ai/sdk/helpers/beta/environments"; import type { BetaSelfHostedWork } from "@anthropic-ai/sdk/resources/beta/environments"; const environmentKey = process.env.ANTHROPIC_ENVIRONMENT_KEY!; const environmentId = process.env.ANTHROPIC_ENVIRONMENT_ID!; const client = new Anthropic({ authToken: environmentKey }); async function launchContainer(work: BetaSelfHostedWork): Promise { // Replace with your own per-session sandbox launcher. Pass // ANTHROPIC_ENVIRONMENT_KEY into the launched sandbox, never // your API key. console.log(`claimed session ${work.data.id}`); } const poller = new WorkPoller({ client, environmentId, environmentKey, autoStop: false // the launched sandbox owns the stop call }); for await (const work of poller) { await launchContainer(work); } ``` ```csharp C# // A work-polling helper is not currently available in the C# SDK. // To claim work directly, see the Environments Work endpoints. ``` ```go Go package main import ( "context" "fmt" "log" "os" "github.com/anthropics/anthropic-sdk-go" "github.com/anthropics/anthropic-sdk-go/lib/environments" "github.com/anthropics/anthropic-sdk-go/option" ) func launchContainer(work *anthropic.BetaSelfHostedWork) { // Replace with your own per-session sandbox launcher. The Go poller // calls work.Stop when this function returns (it has no auto-stop // opt-out), so block here until the session completes rather than // detaching as the Python and TypeScript tabs do. fmt.Printf("claimed session %s\n", work.Data.ID) } func main() { environmentID := os.Getenv("ANTHROPIC_ENVIRONMENT_ID") environmentKey := os.Getenv("ANTHROPIC_ENVIRONMENT_KEY") client := anthropic.NewClient(option.WithAuthToken(environmentKey)) ctx := context.Background() poller := environments.NewWorkPoller(ctx, client, environments.WorkPollerOptions{ EnvironmentID: environmentID, EnvironmentKey: environmentKey, }) defer poller.Close() for work, err := range poller.All() { if err != nil { log.Fatal(err) } launchContainer(work) } } ``` ```java Java // A work-polling helper is not currently available in the Java SDK. // To claim work directly, see the Environments Work endpoints. ``` ```php PHP // A work-polling helper is not currently available in the PHP SDK. // To claim work directly, see the Environments Work endpoints. ``` ```ruby Ruby # A work-polling helper is not currently available in the Ruby SDK. # To claim work directly, see the Environments Work endpoints. ``` **`AgentToolContext`** is the execution context for tool calls. It defines the working directory and path policy, and can download the session's skills. **`beta_agent_toolset_20260401(env)`** takes an `AgentToolContext` and returns the standard tool implementations (`bash`, `read`, `write`, `edit`, `glob`, `grep`). **With `EnvironmentWorker`:** both are managed automatically. Pass a `tools` factory to customize the tool list: ```python Python EnvironmentWorker(client, ..., tools=lambda env: [beta_bash_tool(env), my_custom_tool]) ``` ```typescript TypeScript new EnvironmentWorker({ client, environmentId, environmentKey, tools: (ctx) => [betaBashTool(ctx), myCustomTool] }); ``` ```csharp C# // EnvironmentWorker is not currently available in the C# SDK. // To answer custom tool calls directly, see the session event stream. ``` ```go Go worker := environments.NewEnvironmentWorker(client, environments.EnvironmentWorkerOptions{ EnvironmentID: environmentID, EnvironmentKey: environmentKey, ToolsFunc: func(env *agenttoolset.AgentToolContext) []anthropic.BetaTool { return []anthropic.BetaTool{agenttoolset.BetaBashTool(env), myCustomTool} }, }) ``` ```java Java // EnvironmentWorker is not currently available in the Java SDK. // To answer custom tool calls directly, see the session event stream. ``` ```php PHP // EnvironmentWorker is not currently available in the PHP SDK. // To answer custom tool calls directly, see the session event stream. ``` ```ruby Ruby # EnvironmentWorker is not currently available in the Ruby SDK. # To answer custom tool calls directly, see the session event stream. ``` **With `work.poller()` and `tool_runner()`:** pass a tool list as `tools` to `client.beta.sessions.events.tool_runner()`. To build that list, set up `AgentToolContext` yourself and call `beta_agent_toolset_20260401(env)`: ```python Python from anthropic.lib.tools.agent_toolset import ( AgentToolContext, beta_agent_toolset_20260401, ) async with AgentToolContext( workdir="/workspace", client=client, session_id=work.data.id ) as env: # skills downloaded to /workspace/skills// tools = beta_agent_toolset_20260401(env) ``` ```typescript TypeScript import { setupSkills, betaAgentToolset20260401 } from "@anthropic-ai/sdk/tools/agent-toolset/node"; const ctx = { workdir: "/workspace", client, sessionId: work.data.id }; await setupSkills(ctx); const tools = betaAgentToolset20260401(ctx); ``` ```csharp C# // AgentToolContext is not currently available in the C# SDK. ``` ```go Go env := &agenttoolset.AgentToolContext{Workdir: "/workspace"} if err := env.SetupSkills(ctx, client, work.Data.ID); err != nil { panic(err) } // skills downloaded to /workspace/skills// tools := agenttoolset.BetaAgentToolset20260401(env) ``` ```java Java // AgentToolContext is not currently available in the Java SDK. ``` ```php PHP // AgentToolContext is not currently available in the PHP SDK. ``` ```ruby Ruby # AgentToolContext is not currently available in the Ruby SDK. ``` ### Verify the worker is connected From a separate shell, with `ANTHROPIC_API_KEY` set to your Claude API key (not the environment key), confirm `workers_polling` is at least 1: ```bash ant beta:environments:work stats --environment-id "$ANTHROPIC_ENVIRONMENT_ID" ``` If `workers_polling` stays at 0, the worker isn't reaching the queue: confirm `ANTHROPIC_ENVIRONMENT_KEY` and `ANTHROPIC_ENVIRONMENT_ID` are set on the worker host. See [Read queue depth](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#read-queue-depth) for the full stats response and other language examples. ## Start a session Once your worker is running, create a session that targets the environment. Set `AGENT_ID` to the agent ID you noted in [Before you begin](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#before-you-begin). The session enters the environment's work queue and waits there until a worker claims it; if no worker is connected, the session stays queued rather than failing. Anthropic doesn't mount files or GitHub repositories into self-hosted sandboxes. To make session-specific files available, pass file references (such as an S3 path or commit SHA) in the session `metadata` field. The claimed work item doesn't carry the session's metadata, but it does carry the session ID: your spawn script or `--on-work` handler retrieves the session (`GET /v1/sessions/{session_id}`) to read the `metadata` field, then stages the files into the working directory before tool execution begins. ```bash cURL curl -sS --fail-with-body https://api.anthropic.com/v1/sessions \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d @- < { ["input_file"] = "s3://my-bucket/data.csv" }, }); ``` ```go Go session, err := client.Beta.Sessions.New(ctx, anthropic.BetaSessionNewParams{ Agent: anthropic.BetaSessionNewParamsAgentUnion{OfString: anthropic.String(agent.ID)}, EnvironmentID: environment.ID, Metadata: map[string]string{ "input_file": "s3://my-bucket/data.csv", }, }) if err != nil { panic(err) } ``` ```java Java var session = client.beta().sessions().create(SessionCreateParams.builder() .agent(agent.id()) .environmentId(environment.id()) .metadata(SessionCreateParams.Metadata.builder() .putAdditionalProperty("input_file", JsonValue.from("s3://my-bucket/data.csv")) .build()) .build()); ``` ```php PHP $session = $client->beta->sessions->create( agent: $agent->id, environmentID: $environment->id, metadata: ['input_file' => 's3://my-bucket/data.csv'], ); ``` ```ruby Ruby session = client.beta.sessions.create( agent: agent.id, environment_id: environment.id, metadata: {input_file: "s3://my-bucket/data.csv"} ) ``` Self-hosted sandboxes don't support `resources` entries; a session that includes any resource on a self-hosted environment is rejected. See [Self-hosted worker](https://platform.claude.com/docs/en/managed-agents/reference#self-hosted-worker) in the reference for the full list of CLI flags, and [SDK helpers](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#sdk-helpers) for the SDK helper options. ## Serve custom tools from your sandbox [Custom tools](https://platform.claude.com/docs/en/managed-agents/tools#custom-tools) are tools your own code executes: the agent emits an `agent.custom_tool_use` event and waits for a matching `user.custom_tool_result`. The worker can be that code, and because it runs inside your sandbox, the tool reaches the internal services, credentials, and network egress you configured for the sandbox, and nothing more. The environment key authorizes posting custom tool results, so your Claude API key stays off the worker host. Serving custom tools requires the SDK worker: the `ant` CLI worker has no way to register a custom tool implementation. In the sandbox-per-session pattern, run `EnvironmentWorker` inside the sandbox with `handle_item()` (`handleItem` in TypeScript, `HandleItem` in Go) in place of `ant beta:worker run`. Add a `custom` entry to the agent's `tools` whose `name` matches the tool your worker registers. See [Custom tools](https://platform.claude.com/docs/en/managed-agents/tools#custom-tools) for the full declaration shape. ```json { "type": "custom", "name": "get_order_status", "description": "Look up an order in the internal fulfillment system by order ID.", "input_schema": { "type": "object", "properties": { "order_id": { "type": "string", "description": "The order ID" } }, "required": ["order_id"] } } ``` Pass the tool through the worker's `tools` factory (see [SDK helpers](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#sdk-helpers)), alongside the built-in toolset: ```python Python import asyncio import os from anthropic import AsyncAnthropic, beta_async_tool from anthropic.lib.environments import EnvironmentWorker from anthropic.lib.tools.agent_toolset import beta_agent_toolset_20260401 @beta_async_tool async def get_order_status(order_id: str) -> str: """Look up an order in the internal fulfillment system by order ID.""" # Runs on the worker host: call anything the sandbox can reach. return f"Order {order_id}: shipped" async def main() -> None: environment_key = os.environ["ANTHROPIC_ENVIRONMENT_KEY"] environment_id = os.environ["ANTHROPIC_ENVIRONMENT_ID"] async with AsyncAnthropic(auth_token=environment_key) as client: await EnvironmentWorker( client, environment_id=environment_id, environment_key=environment_key, workdir="/workspace", tools=lambda env: [*beta_agent_toolset_20260401(env), get_order_status], ).run() asyncio.run(main()) ``` ```typescript TypeScript import Anthropic from "@anthropic-ai/sdk"; import { EnvironmentWorker } from "@anthropic-ai/sdk/helpers/beta/environments"; import { betaTool } from "@anthropic-ai/sdk/helpers/beta/json-schema"; import { betaAgentToolset20260401 } from "@anthropic-ai/sdk/tools/agent-toolset/node"; const getOrderStatus = betaTool({ name: "get_order_status", description: "Look up an order in the internal fulfillment system by order ID.", inputSchema: { type: "object", properties: { order_id: { type: "string", description: "The order ID" } }, required: ["order_id"] }, // Runs on the worker host: call anything the sandbox can reach. run: async ({ order_id }) => `Order ${order_id}: shipped` }); const environmentKey = process.env.ANTHROPIC_ENVIRONMENT_KEY!; const environmentId = process.env.ANTHROPIC_ENVIRONMENT_ID!; const client = new Anthropic({ authToken: environmentKey }); const controller = new AbortController(); process.once("SIGTERM", () => controller.abort()); await new EnvironmentWorker({ client, environmentId, environmentKey, workdir: "/workspace", signal: controller.signal, tools: (ctx) => [...betaAgentToolset20260401(ctx), getOrderStatus] }).run(); ``` ```csharp C# // EnvironmentWorker is not currently available in the C# SDK. // To answer custom tool calls directly, see the session event stream. ``` ```go Go package main import ( "context" "log" "os" "os/signal" "syscall" "github.com/anthropics/anthropic-sdk-go" "github.com/anthropics/anthropic-sdk-go/lib/environments" "github.com/anthropics/anthropic-sdk-go/option" "github.com/anthropics/anthropic-sdk-go/toolrunner" "github.com/anthropics/anthropic-sdk-go/tools/agenttoolset" ) type orderStatusInput struct { OrderID string `json:"order_id"` } func main() { environmentKey := os.Getenv("ANTHROPIC_ENVIRONMENT_KEY") environmentID := os.Getenv("ANTHROPIC_ENVIRONMENT_ID") ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() getOrderStatus := toolrunner.NewBetaTool( "get_order_status", "Look up an order in the internal fulfillment system by order ID.", anthropic.BetaToolInputSchemaParam{ Properties: map[string]any{ "order_id": map[string]any{"type": "string", "description": "The order ID"}, }, Required: []string{"order_id"}, }, // Runs on the worker host: call anything the sandbox can reach. func(ctx context.Context, input orderStatusInput) (anthropic.BetaToolResultBlockParamContentUnion, error) { return anthropic.BetaToolResultBlockParamContentUnion{ OfText: &anthropic.BetaTextBlockParam{Text: "Order " + input.OrderID + ": shipped"}, }, nil }, ) client := anthropic.NewClient(option.WithAuthToken(environmentKey)) worker := environments.NewEnvironmentWorker(client, environments.EnvironmentWorkerOptions{ EnvironmentID: environmentID, EnvironmentKey: environmentKey, Workdir: "/workspace", ToolsFunc: func(env *agenttoolset.AgentToolContext) []anthropic.BetaTool { return append(agenttoolset.BetaAgentToolset20260401(env), getOrderStatus) }, }) if err := worker.Run(ctx); err != nil { log.Fatalf("worker: %v", err) } } ``` ```java Java // EnvironmentWorker is not currently available in the Java SDK. // To answer custom tool calls directly, see the session event stream. ``` ```php PHP // EnvironmentWorker is not currently available in the PHP SDK. // To answer custom tool calls directly, see the session event stream. ``` ```ruby Ruby # EnvironmentWorker is not currently available in the Ruby SDK. # To answer custom tool calls directly, see the session event stream. ``` The worker answers only the tools registered with it. A custom tool that is declared on the agent but registered with no worker or client leaves the session paused with a `requires_action` stop reason until something posts its result; see [Handling custom tool calls](https://platform.claude.com/docs/en/managed-agents/events-and-streaming#handling-custom-tool-calls) for the event flow. ### Wrap an MCP server as custom tools The [MCP connector](https://platform.claude.com/docs/en/managed-agents/mcp-connector) connects to MCP servers from Anthropic's side, so a server must expose an HTTP endpoint that Anthropic can reach, directly or through an [MCP tunnel](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/overview). To use a server that only your network can reach, make the worker the MCP client instead and declare the server's tools as custom tools. The MCP server needs no inbound connectivity from outside your network; Anthropic receives the tool definitions you declare on the agent, each call's input, and the result your worker posts back. At runtime the model calls a wrapped tool like any other custom tool: 1. The agent emits an `agent.custom_tool_use` event. 2. The worker, inside your sandbox, forwards the call over its open MCP session to the server on your network. 3. The worker posts the server's response as the `user.custom_tool_result`. The SDKs' [Client-side MCP helpers](https://platform.claude.com/docs/en/agents-and-tools/mcp-connector#client-side-mcp-helpers) convert the server's tools into the runnable tools the worker accepts; install an MCP SDK alongside the Anthropic SDK (`pip install "anthropic[mcp]" "mcp>=1.24"`, `npm install @modelcontextprotocol/sdk`, `go get github.com/modelcontextprotocol/go-sdk`). The examples connect without authentication; to send credentials, configure the HTTP client or request options you hand to the MCP transport (`http_client` in Python, `requestInit` in TypeScript, `HTTPClient` in Go). List the MCP server's tools and declare each one as a `custom` tool; the MCP `name`, `description`, and `inputSchema` map one to one onto the custom tool's fields. If the server paginates its tool list, declare every page; the worker must list the same pages. ```python Python import asyncio from typing import Any, cast from anthropic import AsyncAnthropic from anthropic.types.beta import BetaManagedAgentsCustomToolParams from mcp import ClientSession, types # Requires mcp >= 1.24, which renamed streamablehttp_client to streamable_http_client. from mcp.client.streamable_http import streamable_http_client MCP_SERVER_URL = "http://mcp.internal.example.com:8000/mcp" def to_custom_tool(tool: types.Tool) -> BetaManagedAgentsCustomToolParams: # The MCP fields map one to one onto a custom tool declaration. The cast # hands the schema dictionary to the SDK's typed parameter unchanged. return { "type": "custom", "name": tool.name, "description": tool.description or tool.name, "input_schema": cast(Any, tool.inputSchema), } async def main() -> None: # Run this wherever you create agents, not on the worker host: it # authenticates with your Claude API key (ANTHROPIC_API_KEY). async with ( streamable_http_client(MCP_SERVER_URL) as (read, write, _), ClientSession(read, write) as mcp_session, AsyncAnthropic() as client, ): await mcp_session.initialize() listed = await mcp_session.list_tools() agent = await client.beta.agents.create( name="Internal tools agent", model="claude-opus-5", tools=[ {"type": "agent_toolset_20260401"}, *[to_custom_tool(tool) for tool in listed.tools], ], ) print(agent.id) asyncio.run(main()) ``` ```typescript TypeScript import Anthropic from "@anthropic-ai/sdk"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const MCP_SERVER_URL = "http://mcp.internal.example.com:8000/mcp"; // Run this wherever you create agents, not on the worker host: it // authenticates with your Claude API key (ANTHROPIC_API_KEY). const client = new Anthropic(); const mcpClient = new Client({ name: "declare-agent-tools", version: "1.0.0" }); await mcpClient.connect(new StreamableHTTPClientTransport(new URL(MCP_SERVER_URL))); const { tools } = await mcpClient.listTools(); const agent = await client.beta.agents.create({ name: "Internal tools agent", model: "claude-opus-5", tools: [ { type: "agent_toolset_20260401" }, // The MCP fields map one to one onto a custom tool declaration. ...tools.map((tool) => ({ type: "custom" as const, name: tool.name, description: tool.description || tool.name, input_schema: tool.inputSchema })) ] }); console.log(agent.id); await mcpClient.close(); ``` ```csharp C# // See the Python, TypeScript, and Go tabs. Declaring custom tools from // C# works the same way once you list the server's tools with an MCP client. ``` ```go Go package main import ( "context" "encoding/json" "fmt" "log" "github.com/anthropics/anthropic-sdk-go" mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" ) const mcpServerURL = "http://mcp.internal.example.com:8000/mcp" // toCustomTool maps one MCP tool definition onto a custom tool declaration. // The fields map one to one: the typed parameter carries `properties` and // `required`, and every other JSON Schema keyword the server emits travels in // ExtraFields so the declared schema matches the server's schema. func toCustomTool(tool *mcpsdk.Tool) (anthropic.BetaAgentNewParamsToolUnion, error) { raw, err := json.Marshal(tool.InputSchema) if err != nil { return anthropic.BetaAgentNewParamsToolUnion{}, err } var schema map[string]any if err := json.Unmarshal(raw, &schema); err != nil { return anthropic.BetaAgentNewParamsToolUnion{}, err } inputSchema := anthropic.BetaManagedAgentsCustomToolInputSchemaParam{ExtraFields: map[string]any{}} for keyword, value := range schema { switch keyword { case "type": // The parameter type always marshals "type": "object". case "properties": properties, _ := value.(map[string]any) inputSchema.Properties = properties case "required": entries, _ := value.([]any) for _, entry := range entries { if name, isString := entry.(string); isString { inputSchema.Required = append(inputSchema.Required, name) } } default: inputSchema.ExtraFields[keyword] = value } } description := tool.Description if description == "" { description = tool.Name } return anthropic.BetaAgentNewParamsToolUnion{ OfCustom: &anthropic.BetaManagedAgentsCustomToolParams{ Type: anthropic.BetaManagedAgentsCustomToolParamsTypeCustom, Name: tool.Name, Description: description, InputSchema: inputSchema, }, }, nil } func main() { ctx := context.Background() // Run this wherever you create agents, not on the worker host: it // authenticates with your Claude API key (ANTHROPIC_API_KEY). client := anthropic.NewClient() mcpClient := mcpsdk.NewClient(&mcpsdk.Implementation{Name: "declare-agent-tools", Version: "1.0.0"}, nil) session, err := mcpClient.Connect(ctx, &mcpsdk.StreamableClientTransport{Endpoint: mcpServerURL}, nil) if err != nil { log.Fatalf("connect to MCP server: %v", err) } defer session.Close() listed, err := session.ListTools(ctx, nil) if err != nil { log.Fatalf("list MCP tools: %v", err) } tools := []anthropic.BetaAgentNewParamsToolUnion{ {OfAgentToolset20260401: &anthropic.BetaManagedAgentsAgentToolset20260401Params{ Type: anthropic.BetaManagedAgentsAgentToolset20260401ParamsTypeAgentToolset20260401, }}, } for _, tool := range listed.Tools { custom, err := toCustomTool(tool) if err != nil { log.Fatalf("convert MCP tool %s: %v", tool.Name, err) } tools = append(tools, custom) } agent, err := client.Beta.Agents.New(ctx, anthropic.BetaAgentNewParams{ Name: "Internal tools agent", Model: anthropic.BetaManagedAgentsModelConfigParams{ID: anthropic.BetaManagedAgentsModelClaudeOpus5}, Tools: tools, }) if err != nil { log.Fatalf("create agent: %v", err) } fmt.Println(agent.ID) } ``` ```java Java // See the Python, TypeScript, and Go tabs. Declaring custom tools from // Java works the same way once you list the server's tools with an MCP client. ``` ```php PHP // See the Python, TypeScript, and Go tabs. Declaring custom tools from // PHP works the same way once you list the server's tools with an MCP client. ``` ```ruby Ruby # See the Python, TypeScript, and Go tabs. Declaring custom tools from # Ruby works the same way once you list the server's tools with an MCP client. ``` Connect to the same MCP server at startup, convert its tools with the MCP helpers, and register them alongside the built-in toolset. Keep one MCP session open for the life of the worker. ```python Python import asyncio import os from datetime import timedelta from anthropic import AsyncAnthropic from anthropic.lib.environments import EnvironmentWorker from anthropic.lib.tools.agent_toolset import beta_agent_toolset_20260401 from anthropic.lib.tools.mcp import async_mcp_tool from mcp import ClientSession # Requires mcp >= 1.24, which renamed streamablehttp_client to streamable_http_client. from mcp.client.streamable_http import streamable_http_client MCP_SERVER_URL = "http://mcp.internal.example.com:8000/mcp" async def main() -> None: environment_key = os.environ["ANTHROPIC_ENVIRONMENT_KEY"] environment_id = os.environ["ANTHROPIC_ENVIRONMENT_ID"] # Connect to the MCP server once at startup and keep the session open for # the life of the worker. The timeout turns a hung tool call into an error # result instead of a stalled call. async with ( streamable_http_client(MCP_SERVER_URL) as (read, write, _), ClientSession(read, write, read_timeout_seconds=timedelta(seconds=60)) as mcp_session, AsyncAnthropic(auth_token=environment_key) as client, ): await mcp_session.initialize() listed = await mcp_session.list_tools() mcp_tools = [async_mcp_tool(tool, mcp_session) for tool in listed.tools] await EnvironmentWorker( client, environment_id=environment_id, environment_key=environment_key, workdir="/workspace", tools=lambda env: [*beta_agent_toolset_20260401(env), *mcp_tools], ).run() asyncio.run(main()) ``` ```typescript TypeScript import Anthropic from "@anthropic-ai/sdk"; import { EnvironmentWorker } from "@anthropic-ai/sdk/helpers/beta/environments"; import { mcpTools, type MCPCallToolResultLike, type MCPClientLike } from "@anthropic-ai/sdk/helpers/beta/mcp"; import { betaAgentToolset20260401 } from "@anthropic-ai/sdk/tools/agent-toolset/node"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const MCP_SERVER_URL = "http://mcp.internal.example.com:8000/mcp"; const environmentKey = process.env.ANTHROPIC_ENVIRONMENT_KEY!; const environmentId = process.env.ANTHROPIC_ENVIRONMENT_ID!; const client = new Anthropic({ authToken: environmentKey }); const controller = new AbortController(); process.once("SIGTERM", () => controller.abort()); // Connect to the MCP server once at startup and keep the connection open for // the life of the worker. const mcpClient = new Client({ name: "sandbox-worker", version: "1.0.0" }); await mcpClient.connect(new StreamableHTTPClientTransport(new URL(MCP_SERVER_URL))); const { tools } = await mcpClient.listTools(); // The MCP SDK's callTool return type still includes a legacy result shape that // mcpTools does not accept; narrow it. Drop this once MCPClientLike widens. const mcpClientForTools: MCPClientLike = { callTool: (params) => mcpClient.callTool(params) as Promise }; await new EnvironmentWorker({ client, environmentId, environmentKey, workdir: "/workspace", signal: controller.signal, tools: (ctx) => [...betaAgentToolset20260401(ctx), ...mcpTools(tools, mcpClientForTools)] }).run(); ``` ```csharp C# // EnvironmentWorker is not currently available in the C# SDK. ``` ```go Go package main import ( "context" "log" "os" "os/signal" "syscall" "github.com/anthropics/anthropic-sdk-go" "github.com/anthropics/anthropic-sdk-go/lib/environments" "github.com/anthropics/anthropic-sdk-go/mcp" "github.com/anthropics/anthropic-sdk-go/option" "github.com/anthropics/anthropic-sdk-go/tools/agenttoolset" mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" ) const mcpServerURL = "http://mcp.internal.example.com:8000/mcp" func main() { environmentKey := os.Getenv("ANTHROPIC_ENVIRONMENT_KEY") environmentID := os.Getenv("ANTHROPIC_ENVIRONMENT_ID") ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() client := anthropic.NewClient(option.WithAuthToken(environmentKey)) // Connect to the MCP server once at startup and keep the session open for // the life of the worker. mcpClient := mcpsdk.NewClient(&mcpsdk.Implementation{Name: "sandbox-worker", Version: "1.0.0"}, nil) session, err := mcpClient.Connect(ctx, &mcpsdk.StreamableClientTransport{Endpoint: mcpServerURL}, nil) if err != nil { log.Fatalf("connect to MCP server: %v", err) } defer session.Close() listed, err := session.ListTools(ctx, nil) if err != nil { log.Fatalf("list MCP tools: %v", err) } mcpTools, err := mcp.NewBetaTools(listed.Tools, session) if err != nil { log.Fatalf("convert MCP tools: %v", err) } worker := environments.NewEnvironmentWorker(client, environments.EnvironmentWorkerOptions{ EnvironmentID: environmentID, EnvironmentKey: environmentKey, Workdir: "/workspace", ToolsFunc: func(env *agenttoolset.AgentToolContext) []anthropic.BetaTool { return append(agenttoolset.BetaAgentToolset20260401(env), mcpTools...) }, }) if err := worker.Run(ctx); err != nil { log.Fatalf("worker: %v", err) } } ``` ```java Java // EnvironmentWorker is not currently available in the Java SDK. ``` ```php PHP // EnvironmentWorker is not currently available in the PHP SDK. ``` ```ruby Ruby # EnvironmentWorker is not currently available in the Ruby SDK. ``` Keep the following in mind when you wrap an MCP server: * **Tools are declared, not discovered at runtime.** The worker lists the MCP server's tools once at startup and cannot add tools to a running session. When the server's tools change, declare them again, on the agent or on an idle session through [Updating the agent configuration](https://platform.claude.com/docs/en/managed-agents/session-operations#updating-the-agent-configuration), and restart the worker. * **Names and descriptions must fit the Managed Agents API.** Custom tool names are unique per agent and use letters, digits, underscores, and hyphens (1–128 characters); a non-empty description is required; and an agent's `tools` array takes at most 128 entries (each wrapped tool is one entry, and the built-in toolset is one more). The API rejects a declaration that reuses a tool name, names a custom tool after a built-in agent tool such as `bash` or `read`, or uses the reserved `mcp__` prefix. The MCP helpers keep the server's names and descriptions, so rename or trim where needed. When two servers expose the same tool name, define the wrapper yourself under a prefixed name and have it call the server's original tool name. * **Most schemas pass through unchanged.** The API accepts the JSON Schema keywords MCP servers commonly emit, such as `additionalProperties` and `title`. It rejects reference keywords such as `$ref` anywhere in a custom tool's `input_schema`, so inline the schemas that generators such as pydantic factor into `$defs`. It also rejects top-level `oneOf`, `anyOf`, and `allOf`, and property names outside letters, digits, underscores, dots, and hyphens (1–64 characters). * **Tool failures surface as error tool results.** When the MCP server reports a tool error, the worker posts an error tool result the model can react to. MCP content with no tool result equivalent, such as audio blocks and resource links, also surfaces as an error. Set a timeout on the MCP client for a faster and clearer failure, as the Python worker example does with `read_timeout_seconds`. Without one, a hung call becomes an error result only when the TypeScript MCP SDK's default request timeout fires (about a minute) or when the worker's own backstop does: about two and a half minutes in Python, and two minutes in Go, where the worker cancels a tool call that outlives its 120-second default and posts an error result. * **Wrap servers you operate or trust.** A wrapped tool's name, description, and results enter the model's context like any other tool's: untrusted input that can influence what the agent does with its other tools, including `bash` on the worker host. Declare only the tools you intend the agent to use. * **Permission policies do not apply to custom tools.** [Permission policies](https://platform.claude.com/docs/en/managed-agents/permission-policies#custom-tools) govern the built-in and MCP toolsets; the worker executes every wrapped tool call the model makes, so put any approval step in your own tool code. ## Monitoring and operations These calls run from your monitoring or operations tooling, authenticated with your Claude API key, to observe and manage the worker fleet. The claim and keep-alive loop is handled inside the worker helpers, so you don't call those endpoints directly. These endpoints accept either your organization API key or the environment key. Call them from outside the worker host with your organization API key. Setting `ANTHROPIC_API_KEY` on the worker host exposes an organization-scoped credential to agent tool calls. ### Read queue depth `work.stats` returns the queue state for an environment: * `depth` is the number of items waiting to be claimed. Scale your worker fleet or alert on backlog based on this value. * `pending` is the number of items claimed by a worker but not yet acknowledged. The worker helpers acknowledge each item before processing it, so this value stays near zero in normal operation; a sustained non-zero value means a worker stalled between claiming and acknowledging. * `oldest_queued_at` is the timestamp of the oldest item still in the queue, waiting to be claimed or claimed but not yet acknowledged, or `null` when there is none. * `workers_polling` is the number of workers that have polled in the last 30 seconds. Use this for liveness alerting. ```bash cURL curl -sS "https://api.anthropic.com/v1/environments/$ANTHROPIC_ENVIRONMENT_ID/work/stats" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "anthropic-version: 2023-06-01" ``` ```bash CLI ant beta:environments:work stats --environment-id "$ANTHROPIC_ENVIRONMENT_ID" ``` ```python Python import os import anthropic client = anthropic.Anthropic() stats = client.beta.environments.work.stats(os.environ["ANTHROPIC_ENVIRONMENT_ID"]) print(f"depth={stats.depth} pending={stats.pending}") ``` ```typescript TypeScript import Anthropic from "@anthropic-ai/sdk"; const client = new Anthropic(); const stats = await client.beta.environments.work.stats(process.env.ANTHROPIC_ENVIRONMENT_ID!); console.log(`depth=${stats.depth} pending=${stats.pending}`); ``` ```csharp C# using Anthropic; var client = new AnthropicClient(); var environmentId = Environment.GetEnvironmentVariable("ANTHROPIC_ENVIRONMENT_ID")!; var stats = await client.Beta.Environments.Work.Stats(environmentId); Console.WriteLine($"depth={stats.Depth} pending={stats.Pending}"); ``` ```go Go package main import ( "context" "fmt" "os" "github.com/anthropics/anthropic-sdk-go" ) func main() { client := anthropic.NewClient() environmentID := os.Getenv("ANTHROPIC_ENVIRONMENT_ID") stats, err := client.Beta.Environments.Work.Stats( context.Background(), environmentID, anthropic.BetaEnvironmentWorkStatsParams{}, ) if err != nil { panic(err) } fmt.Printf("depth=%d pending=%d\n", stats.Depth, stats.Pending) } ``` ```java Java import com.anthropic.client.AnthropicClient; import com.anthropic.client.okhttp.AnthropicOkHttpClient; import com.anthropic.models.beta.environments.work.BetaSelfHostedWorkQueueStats; void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); BetaSelfHostedWorkQueueStats stats = client.beta() .environments() .work() .stats(System.getenv("ANTHROPIC_ENVIRONMENT_ID")); IO.println("depth=" + stats.depth() + " pending=" + stats.pending()); } ``` ```php PHP beta->environments->work->stats(getenv('ANTHROPIC_ENVIRONMENT_ID')); printf("depth=%d pending=%d\n", $stats->depth, $stats->pending); ``` ```ruby Ruby require "anthropic" client = Anthropic::Client.new stats = client.beta.environments.work.stats(ENV.fetch("ANTHROPIC_ENVIRONMENT_ID")) puts "depth=#{stats.depth} pending=#{stats.pending}" ``` ```text wrap { "type": "work_queue_stats", "depth": 0, "pending": 0, "oldest_queued_at": null, "workers_polling": 0 } ``` ### Stop a session gracefully Use `work.stop` to ask the worker handling a specific session to shut it down. By default the work item moves to `stopping`: the worker notices on its next lease heartbeat, cancels the session's in-flight tool call, and confirms the shutdown, at which point the work item becomes `stopped`. Pass `force: true` in the request body (with the CLI, pass `--force`) to mark the work item `stopped` immediately instead of waiting for the worker's confirmation. Because these calls run from your operations tooling rather than the worker host, `ANTHROPIC_WORK_ID` isn't set automatically. Set it to the target work item's ID before running the following examples. To find a work item's ID, list the environment's work items through the [Environments Work endpoints](https://platform.claude.com/docs/en/api/beta/environments/work). ```bash cURL curl -sS "https://api.anthropic.com/v1/environments/$ANTHROPIC_ENVIRONMENT_ID/work/$ANTHROPIC_WORK_ID/stop" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{}' ``` ```bash CLI ant beta:environments:work stop \ --environment-id "$ANTHROPIC_ENVIRONMENT_ID" \ --work-id "$ANTHROPIC_WORK_ID" ``` ```python Python import os import anthropic client = anthropic.Anthropic() work = client.beta.environments.work.stop( os.environ["ANTHROPIC_WORK_ID"], environment_id=os.environ["ANTHROPIC_ENVIRONMENT_ID"], ) print(work.state) ``` ```typescript TypeScript import Anthropic from "@anthropic-ai/sdk"; const client = new Anthropic(); const work = await client.beta.environments.work.stop(process.env.ANTHROPIC_WORK_ID!, { environment_id: process.env.ANTHROPIC_ENVIRONMENT_ID! }); console.log(work.state); ``` ```csharp C# using Anthropic; var client = new AnthropicClient(); var work = await client.Beta.Environments.Work.Stop( Environment.GetEnvironmentVariable("ANTHROPIC_WORK_ID")!, new() { EnvironmentID = Environment.GetEnvironmentVariable("ANTHROPIC_ENVIRONMENT_ID")! } ); Console.WriteLine(work.State); ``` ```go Go package main import ( "context" "fmt" "os" "github.com/anthropics/anthropic-sdk-go" ) func main() { client := anthropic.NewClient() work, err := client.Beta.Environments.Work.Stop( context.Background(), os.Getenv("ANTHROPIC_WORK_ID"), anthropic.BetaEnvironmentWorkStopParams{ EnvironmentID: os.Getenv("ANTHROPIC_ENVIRONMENT_ID"), }, ) if err != nil { panic(err) } fmt.Println(work.State) } ``` ```java Java import com.anthropic.client.AnthropicClient; import com.anthropic.client.okhttp.AnthropicOkHttpClient; import com.anthropic.models.beta.environments.work.BetaSelfHostedWork; import com.anthropic.models.beta.environments.work.BetaSelfHostedWorkStopRequest; import com.anthropic.models.beta.environments.work.WorkStopParams; void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); BetaSelfHostedWork work = client.beta().environments().work().stop( WorkStopParams.builder() .environmentId(System.getenv("ANTHROPIC_ENVIRONMENT_ID")) .workId(System.getenv("ANTHROPIC_WORK_ID")) .betaSelfHostedWorkStopRequest(BetaSelfHostedWorkStopRequest.builder().build()) .build() ); IO.println(work.state()); } ``` ```php PHP beta->environments->work->stop( getenv('ANTHROPIC_WORK_ID'), environmentID: getenv('ANTHROPIC_ENVIRONMENT_ID'), ); echo $work->state . "\n"; ``` ```ruby Ruby require "anthropic" client = Anthropic::Client.new work = client.beta.environments.work.stop( ENV.fetch("ANTHROPIC_WORK_ID"), environment_id: ENV.fetch("ANTHROPIC_ENVIRONMENT_ID") ) puts work.state ``` ## Next steps Shared responsibility model for self-hosted sandbox environments. Create a session to run your agent and begin executing tasks. Securely connect Claude to MCP servers running in your private network without opening inbound ports or exposing services to the public internet. ### Delegate work to your agent --- title: Authenticate with vaults url: https://platform.claude.com/docs/en/managed-agents/vaults description: Register per-user credentials when creating sessions. --- Vaults and credentials are authentication primitives that let you register credentials for third-party services once and reference them by ID at session creation. This means you don't need to run your own secret store, transmit tokens on every call, or lose track of which end user an agent acted on behalf of. The vault reference is a per-session parameter, so you can manage your product at the `agent` resource granularity and your users at the `session` resource granularity. Managed Agents API requests require the `managed-agents-2026-04-01` beta header, except memory store endpoints, which use `agent-memory-2026-07-22` instead. The SDK sets the correct beta header automatically. See [Beta headers](https://platform.claude.com/docs/en/api/beta-headers#endpoint-specific-headers). ## Create a vault Vaults and credentials are workspace-scoped, meaning anyone with an API key for the same workspace can reference them when creating a session. To revoke access, delete the vault or credential. A vault is the collection of `credentials` associated with an end user. Give it a `display_name` and optionally tag it with `metadata` so you can map it back to your own user records. ```bash cURL vault_id=$(curl --fail-with-body -sS https://api.anthropic.com/v1/vaults \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ --data @- <<'EOF' | jq -r '.id' { "display_name": "Alice", "metadata": {"external_user_id": "usr_abc123"} } EOF ) echo "$vault_id" # "vlt_01ABC..." ``` ```bash CLI VAULT_ID=$(ant beta:vaults create \ --display-name "Alice" \ --metadata '{external_user_id: usr_abc123}' \ --transform id --raw-output) echo "$VAULT_ID" # "vlt_01ABC..." ``` ```python Python vault = client.beta.vaults.create( display_name="Alice", metadata={"external_user_id": "usr_abc123"}, ) print(vault.id) # "vlt_01ABC..." ``` ```typescript TypeScript const vault = await client.beta.vaults.create({ display_name: "Alice", metadata: { external_user_id: "usr_abc123" }, }); console.log(vault.id); // "vlt_01ABC..." ``` ```csharp C# var vault = await client.Beta.Vaults.Create(new() { DisplayName = "Alice", Metadata = new Dictionary { ["external_user_id"] = "usr_abc123" }, }); Console.WriteLine(vault.ID); // "vlt_01ABC..." ``` ```go Go vault, err := client.Beta.Vaults.New(ctx, anthropic.BetaVaultNewParams{ DisplayName: "Alice", Metadata: map[string]string{"external_user_id": "usr_abc123"}, }) if err != nil { panic(err) } fmt.Println(vault.ID) // "vlt_01ABC..." ``` ```java Java var vault = client.beta().vaults().create(VaultCreateParams.builder() .displayName("Alice") .metadata(VaultCreateParams.Metadata.builder() .putAdditionalProperty("external_user_id", JsonValue.from("usr_abc123")) .build()) .build()); IO.println(vault.id()); // "vlt_01ABC..." ``` ```php PHP $vault = $client->beta->vaults->create( displayName: 'Alice', metadata: ['external_user_id' => 'usr_abc123'], ); echo $vault->id . "\n"; // "vlt_01ABC..." ``` ```ruby Ruby vault = client.beta.vaults.create( display_name: "Alice", metadata: {external_user_id: "usr_abc123"} ) puts vault.id # "vlt_01ABC..." ``` The response is the full vault record: ```json { "type": "vault", "id": "vlt_01ABC...", "display_name": "Alice", "metadata": { "external_user_id": "usr_abc123" }, "created_at": "2026-03-18T10:00:00Z", "updated_at": "2026-03-18T10:00:00Z", "archived_at": null } ``` ## Add a credential Two credential categories are supported: * **MCP credentials** (`mcp_oauth`, `static_bearer`): each credential is keyed by an `mcp_server_url`. When the agent connects to a server at that URL at session runtime, the token is injected automatically. * **Environment variables** (`environment_variable`): each credential is keyed by a `secret_name` (the environment variable name) and stored in the sandbox as an opaque placeholder. When the agent initiates an outbound request, the opaque placeholder is substituted with the real secret at egress. The agent never sees the secret value. Use this for any service that authenticates through an environment variable, such as CLIs, SDKs, or direct API calls. The actual credential values you supply (`token`, `access_token`, `refresh_token`, `client_secret`, `secret_value`) are treated as sensitive, write-only fields and never returned in API responses. Environment variable credentials (`environment_variable`) are not yet supported with [self-hosted sandboxes](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes). Use `mcp_oauth` when the MCP server uses OAuth 2.0. If you supply a `refresh` block, Anthropic refreshes the access token on your behalf when it expires. The `refresh.token_endpoint_auth.type` field indicates how to authenticate the refresh call: * `none`: public client * `client_secret_basic`: HTTP Basic authentication with the client secret * `client_secret_post`: client secret in the POST body ```bash cURL credential_id=$(curl --fail-with-body -sS "https://api.anthropic.com/v1/vaults/$vault_id/credentials" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ --data @- <<'EOF' | jq -r '.id' { "display_name": "Alice's Slack", "auth": { "type": "mcp_oauth", "mcp_server_url": "https://mcp.slack.com/mcp", "access_token": "xoxp-...", "expires_at": "2099-12-31T23:59:59Z", "refresh": { "token_endpoint": "https://slack.com/api/oauth.v2.access", "client_id": "1234567890.0987654321", "scope": "channels:read chat:write", "refresh_token": "xoxe-1-...", "token_endpoint_auth": {"type": "client_secret_post", "client_secret": "abc123..."} } } } EOF ) ``` ```bash CLI CREDENTIAL_ID=$(ant beta:vaults:credentials create \ --vault-id "$VAULT_ID" \ --display-name "Alice's Slack" \ --transform id --raw-output <<'YAML' auth: type: mcp_oauth mcp_server_url: https://mcp.slack.com/mcp access_token: xoxp-... expires_at: "2099-12-31T23:59:59Z" refresh: token_endpoint: https://slack.com/api/oauth.v2.access client_id: "1234567890.0987654321" scope: channels:read chat:write refresh_token: xoxe-1-... token_endpoint_auth: type: client_secret_post client_secret: abc123... YAML ) ``` ```python Python credential = client.beta.vaults.credentials.create( vault_id=vault.id, display_name="Alice's Slack", auth={ "type": "mcp_oauth", "mcp_server_url": "https://mcp.slack.com/mcp", "access_token": "xoxp-...", "expires_at": "2099-12-31T23:59:59Z", "refresh": { "token_endpoint": "https://slack.com/api/oauth.v2.access", "client_id": "1234567890.0987654321", "scope": "channels:read chat:write", "refresh_token": "xoxe-1-...", "token_endpoint_auth": {"type": "client_secret_post", "client_secret": "abc123..."}, }, }, ) ``` ```typescript TypeScript const credential = await client.beta.vaults.credentials.create(vault.id, { display_name: "Alice's Slack", auth: { type: "mcp_oauth", mcp_server_url: "https://mcp.slack.com/mcp", access_token: "xoxp-...", expires_at: "2099-12-31T23:59:59Z", refresh: { token_endpoint: "https://slack.com/api/oauth.v2.access", client_id: "1234567890.0987654321", scope: "channels:read chat:write", refresh_token: "xoxe-1-...", token_endpoint_auth: { type: "client_secret_post", client_secret: "abc123...", }, }, }, }); ``` ```csharp C# var credential = await client.Beta.Vaults.Credentials.Create(vault.ID, new() { DisplayName = "Alice's Slack", Auth = new BetaManagedAgentsMcpOAuthCreateParams { Type = BetaManagedAgentsMcpOAuthCreateParamsType.McpOAuth, McpServerUrl = "https://mcp.slack.com/mcp", AccessToken = "xoxp-...", ExpiresAt = DateTimeOffset.Parse("2099-12-31T23:59:59Z"), Refresh = new() { TokenEndpoint = "https://slack.com/api/oauth.v2.access", ClientID = "1234567890.0987654321", Scope = "channels:read chat:write", RefreshToken = "xoxe-1-...", TokenEndpointAuth = new BetaManagedAgentsTokenEndpointAuthPostParam { Type = BetaManagedAgentsTokenEndpointAuthPostParamType.ClientSecretPost, ClientSecret = "abc123...", }, }, }, }); ``` ```go Go credential, err := client.Beta.Vaults.Credentials.New(ctx, vault.ID, anthropic.BetaVaultCredentialNewParams{ DisplayName: anthropic.String("Alice's Slack"), Auth: anthropic.BetaVaultCredentialNewParamsAuthUnion{ OfMCPOAuth: &anthropic.BetaManagedAgentsMCPOAuthCreateParams{ Type: anthropic.BetaManagedAgentsMCPOAuthCreateParamsTypeMCPOAuth, MCPServerURL: "https://mcp.slack.com/mcp", AccessToken: "xoxp-...", ExpiresAt: anthropic.Time(time.Date(2099, time.December, 31, 23, 59, 59, 0, time.UTC)), Refresh: anthropic.BetaManagedAgentsMCPOAuthRefreshParams{ TokenEndpoint: "https://slack.com/api/oauth.v2.access", ClientID: "1234567890.0987654321", Scope: anthropic.String("channels:read chat:write"), RefreshToken: "xoxe-1-...", TokenEndpointAuth: anthropic.BetaManagedAgentsMCPOAuthRefreshParamsTokenEndpointAuthUnion{ OfClientSecretPost: &anthropic.BetaManagedAgentsTokenEndpointAuthPostParam{ Type: anthropic.BetaManagedAgentsTokenEndpointAuthPostParamTypeClientSecretPost, ClientSecret: "abc123...", }, }, }, }, }, }) if err != nil { panic(err) } ``` ```java Java var credential = client.beta().vaults().credentials().create(vault.id(), CredentialCreateParams.builder() .displayName("Alice's Slack") .auth(BetaManagedAgentsMcpOAuthCreateParams.builder() .type(BetaManagedAgentsMcpOAuthCreateParams.Type.MCP_OAUTH) .mcpServerUrl("https://mcp.slack.com/mcp") .accessToken("xoxp-...") .expiresAt(OffsetDateTime.parse("2099-12-31T23:59:59Z")) .refresh(BetaManagedAgentsMcpOAuthRefreshParams.builder() .tokenEndpoint("https://slack.com/api/oauth.v2.access") .clientId("1234567890.0987654321") .scope("channels:read chat:write") .refreshToken("xoxe-1-...") .clientSecretPostTokenEndpointAuth("abc123...") .build()) .build()) .build()); ``` ```php PHP $credential = $client->beta->vaults->credentials->create( vaultID: $vault->id, displayName: "Alice's Slack", auth: ManagedAgentsMCPOAuthCreateParams::with( type: 'mcp_oauth', mcpServerURL: 'https://mcp.slack.com/mcp', accessToken: 'xoxp-...', expiresAt: new DateTimeImmutable('2099-12-31T23:59:59Z'), refresh: ManagedAgentsMCPOAuthRefreshParams::with( tokenEndpoint: 'https://slack.com/api/oauth.v2.access', clientID: '1234567890.0987654321', scope: 'channels:read chat:write', refreshToken: 'xoxe-1-...', tokenEndpointAuth: ManagedAgentsTokenEndpointAuthPostParam::with( type: 'client_secret_post', clientSecret: 'abc123...', ), ), ), ); ``` ```ruby Ruby credential = client.beta.vaults.credentials.create( vault.id, display_name: "Alice's Slack", auth: { type: "mcp_oauth", mcp_server_url: "https://mcp.slack.com/mcp", access_token: "xoxp-...", expires_at: "2099-12-31T23:59:59Z", refresh: { token_endpoint: "https://slack.com/api/oauth.v2.access", client_id: "1234567890.0987654321", scope: "channels:read chat:write", refresh_token: "xoxe-1-...", token_endpoint_auth: { type: "client_secret_post", client_secret: "abc123..." } } } ) ``` Use `static_bearer` when the MCP server accepts a fixed bearer token (API key, personal access token, or similar). No refresh flow is needed. ```bash cURL curl --fail-with-body -sS "https://api.anthropic.com/v1/vaults/$vault_id/credentials" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ --data @- <<'EOF' { "display_name": "Linear API key", "auth": { "type": "static_bearer", "mcp_server_url": "https://mcp.linear.app/mcp", "token": "lin_api_your_linear_key" } } EOF ``` ```bash CLI ant beta:vaults:credentials create --vault-id "$VAULT_ID" <<'YAML' display_name: Linear API key auth: type: static_bearer mcp_server_url: https://mcp.linear.app/mcp token: lin_api_your_linear_key YAML ``` ```python Python bearer_credential = client.beta.vaults.credentials.create( vault_id=vault.id, display_name="Linear API key", auth={ "type": "static_bearer", "mcp_server_url": "https://mcp.linear.app/mcp", "token": "lin_api_your_linear_key", }, ) ``` ```typescript TypeScript const bearerCredential = await client.beta.vaults.credentials.create(vault.id, { display_name: "Linear API key", auth: { type: "static_bearer", mcp_server_url: "https://mcp.linear.app/mcp", token: "lin_api_your_linear_key", }, }); ``` ```csharp C# var bearerCredential = await client.Beta.Vaults.Credentials.Create(vault.ID, new() { DisplayName = "Linear API key", Auth = new BetaManagedAgentsStaticBearerCreateParams { Type = BetaManagedAgentsStaticBearerCreateParamsType.StaticBearer, McpServerUrl = "https://mcp.linear.app/mcp", Token = "lin_api_your_linear_key", }, }); ``` ```go Go bearerCredential, err := client.Beta.Vaults.Credentials.New(ctx, vault.ID, anthropic.BetaVaultCredentialNewParams{ DisplayName: anthropic.String("Linear API key"), Auth: anthropic.BetaVaultCredentialNewParamsAuthUnion{ OfStaticBearer: &anthropic.BetaManagedAgentsStaticBearerCreateParams{ Type: anthropic.BetaManagedAgentsStaticBearerCreateParamsTypeStaticBearer, MCPServerURL: "https://mcp.linear.app/mcp", Token: "lin_api_your_linear_key", }, }, }) if err != nil { panic(err) } _ = bearerCredential ``` ```java Java var bearerCredential = client.beta().vaults().credentials().create(vault.id(), CredentialCreateParams.builder() .displayName("Linear API key") .auth(BetaManagedAgentsStaticBearerCreateParams.builder() .type(BetaManagedAgentsStaticBearerCreateParams.Type.STATIC_BEARER) .mcpServerUrl("https://mcp.linear.app/mcp") .token("lin_api_your_linear_key") .build()) .build()); ``` ```php PHP $bearerCredential = $client->beta->vaults->credentials->create( vaultID: $vault->id, displayName: 'Linear API key', auth: ManagedAgentsStaticBearerCreateParams::with( type: 'static_bearer', mcpServerURL: 'https://mcp.linear.app/mcp', token: 'lin_api_your_linear_key', ), ); ``` ```ruby Ruby bearer_credential = client.beta.vaults.credentials.create( vault.id, display_name: "Linear API key", auth: { type: "static_bearer", mcp_server_url: "https://mcp.linear.app/mcp", token: "lin_api_your_linear_key" } ) ``` Use `environment_variable` to authenticate to external services through an environment variable, such as CLIs, SDKs, or direct API calls. Environment variable credentials work for clients that send the secret value verbatim in an outbound request, so check the client eligibility criteria in this tab before configuring one. The `networking.allowed_hosts` array controls which outbound hosts the secret can be substituted for. Use `"type": "limited"` with a specific list, or `"type": "unrestricted"` if the caller reaches domains you can't enumerate in advance. Limiting domains is strongly recommended for security purposes, and prevents your key from ever being shared with unauthorized hosts. `networking.allowed_hosts` on a vault credential controls which requests use the secret, not which requests are allowed. For the agent to actually reach a domain, it must also be allowed at the [environment level](https://platform.claude.com/docs/en/managed-agents/environments). Both levels must include the domain (either through `unrestricted` networking or by explicitly listing the domain in `allowed_hosts`) for a secret-substituted request to succeed. The optional `injection_location` field scopes where the secret is substituted; the full semantics follow the example. ```bash cURL curl --fail-with-body -sS "https://api.anthropic.com/v1/vaults/$vault_id/credentials" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ --data @- <<'EOF' | jq '.auth.injection_location' { "auth": { "type": "environment_variable", "secret_name": "NOTION_API_KEY", "secret_value": "ntn_your-secret-here", "networking": { "type": "limited", "allowed_hosts": ["api.notion.com"] }, "injection_location": {"header": true} }, "display_name": "Notion API key for sandbox" } EOF ``` ```bash CLI ant beta:vaults:credentials create \ --vault-id "$VAULT_ID" \ --transform 'auth.injection_location' --format json <<'YAML' display_name: Notion API key for sandbox auth: type: environment_variable secret_name: NOTION_API_KEY secret_value: ntn_your-secret-here injection_location: header: true networking: type: limited allowed_hosts: [api.notion.com] YAML ``` ```python Python env_credential = client.beta.vaults.credentials.create( vault_id=vault.id, display_name="Notion API key for sandbox", auth={ "type": "environment_variable", "secret_name": "NOTION_API_KEY", "secret_value": "ntn_your-secret-here", "networking": { "type": "limited", "allowed_hosts": ["api.notion.com"], }, "injection_location": {"header": True}, }, ) if env_credential.auth.type == "environment_variable": location = env_credential.auth.injection_location print(f"header: {location.header}, body: {location.body}") # header: True, body: False ``` ```typescript TypeScript const envVarCredential = await client.beta.vaults.credentials.create(vault.id, { display_name: "Notion API key for sandbox", auth: { type: "environment_variable", secret_name: "NOTION_API_KEY", secret_value: "ntn_your-secret-here", networking: { type: "limited", allowed_hosts: ["api.notion.com"], }, injection_location: { header: true }, }, }); if (envVarCredential.auth.type === "environment_variable") { console.log(envVarCredential.auth.injection_location); // { header: true, body: false } } ``` ```csharp C# var envVarCredential = await client.Beta.Vaults.Credentials.Create(vault.ID, new() { DisplayName = "Notion API key for sandbox", Auth = new BetaManagedAgentsEnvironmentVariableCreateParams { Type = BetaManagedAgentsEnvironmentVariableCreateParamsType.EnvironmentVariable, SecretName = "NOTION_API_KEY", SecretValue = "ntn_your-secret-here", Networking = new BetaManagedAgentsLimitedCredentialNetworkingParams { Type = BetaManagedAgentsLimitedCredentialNetworkingParamsType.Limited, AllowedHosts = ["api.notion.com"], }, InjectionLocation = new() { Header = true }, }, }); if (envVarCredential.Auth.TryPickBetaManagedAgentsEnvironmentVariableAuthResponse(out var envVarAuth)) { var injectionLocation = envVarAuth.InjectionLocation; Console.WriteLine($"Header: {injectionLocation.Header}, Body: {injectionLocation.Body}"); // "Header: True, Body: False" } ``` ```go Go envVarCredential, err := client.Beta.Vaults.Credentials.New(ctx, vault.ID, anthropic.BetaVaultCredentialNewParams{ DisplayName: anthropic.String("Notion API key for sandbox"), Auth: anthropic.BetaVaultCredentialNewParamsAuthUnion{ OfEnvironmentVariable: &anthropic.BetaManagedAgentsEnvironmentVariableCreateParams{ Type: anthropic.BetaManagedAgentsEnvironmentVariableCreateParamsTypeEnvironmentVariable, SecretName: "NOTION_API_KEY", SecretValue: "ntn_your-secret-here", Networking: anthropic.BetaManagedAgentsCredentialNetworkingParamsUnion{ OfLimited: &anthropic.BetaManagedAgentsLimitedCredentialNetworkingParams{ Type: anthropic.BetaManagedAgentsLimitedCredentialNetworkingParamsTypeLimited, AllowedHosts: []string{"api.notion.com"}, }, }, InjectionLocation: anthropic.BetaManagedAgentsInjectionLocationParams{ Header: anthropic.Bool(true), }, }, }, }) if err != nil { panic(err) } if envVarAuth, ok := envVarCredential.Auth.AsAny().(anthropic.BetaManagedAgentsEnvironmentVariableAuthResponse); ok { injectionLocation := envVarAuth.InjectionLocation fmt.Printf("Header:%t Body:%t\n", injectionLocation.Header, injectionLocation.Body) // "Header:true Body:false" } ``` ```java Java var envVarCredential = client.beta().vaults().credentials().create(vault.id(), CredentialCreateParams.builder() .displayName("Notion API key for sandbox") .auth(BetaManagedAgentsEnvironmentVariableCreateParams.builder() .type(BetaManagedAgentsEnvironmentVariableCreateParams.Type.ENVIRONMENT_VARIABLE) .secretName("NOTION_API_KEY") .secretValue("ntn_your-secret-here") .limitedNetworking(List.of("api.notion.com")) .injectionLocation(BetaManagedAgentsInjectionLocationParams.builder() .header(true) .build()) .build()) .build()); envVarCredential.auth().environmentVariable().ifPresent(envVarAuth -> { var injectionLocation = envVarAuth.injectionLocation(); IO.println("header=" + injectionLocation.header() + " body=" + injectionLocation.body()); // header=true body=false }); ``` ```php PHP $envVarCredential = $client->beta->vaults->credentials->create( vaultID: $vault->id, displayName: 'Notion API key for sandbox', auth: ManagedAgentsEnvironmentVariableCreateParams::with( type: ManagedAgentsEnvironmentVariableCreateParams\Type::ENVIRONMENT_VARIABLE, secretName: 'NOTION_API_KEY', secretValue: 'ntn_your-secret-here', networking: ManagedAgentsLimitedCredentialNetworkingParams::with( type: ManagedAgentsLimitedCredentialNetworkingParams\Type::LIMITED, allowedHosts: ['api.notion.com'], ), injectionLocation: ManagedAgentsInjectionLocationParams::with(header: true), ), ); if ($envVarCredential->auth instanceof ManagedAgentsEnvironmentVariableAuthResponse) { $injectionLocation = $envVarCredential->auth->injectionLocation; echo 'header: ' . json_encode($injectionLocation->header) . "\n"; // header: true echo 'body: ' . json_encode($injectionLocation->body) . "\n"; // body: false } ``` ```ruby Ruby env_credential = client.beta.vaults.credentials.create( vault.id, display_name: "Notion API key for sandbox", auth: { type: "environment_variable", secret_name: "NOTION_API_KEY", secret_value: "ntn_your-secret-here", networking: { type: "limited", allowed_hosts: ["api.notion.com"] }, injection_location: {header: true} } ) if env_credential.auth.type == :environment_variable env_credential.auth.injection_location => {header:, body:} puts "header: #{header}, body: #{body}" # header: true, body: false end ``` Request payloads are often assembled from content the agent is working with, so the request body is the broader exposure surface. Most services read an API key from a request header, so enabling only `header` is the narrower configuration. It scopes substitution to request header values for that credential. The credential's `injection_location` controls which parts of an outbound request the secret is substituted into. It is an optional object, a sibling of `networking`, with two Boolean fields: `header` (request headers) and `body` (request body). `injection_location` is independent of `networking.allowed_hosts`: `allowed_hosts` scopes which hosts the secret is substituted for, and `injection_location` scopes which parts of the request it is substituted into. `injection_location` behaves differently on create and on update: | Operation | `injection_location` behavior | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Create credential | If you provide the object, any field you omit inside it defaults to `false`: `{"header": true}` creates a header-only credential. Omit the object entirely and both locations are enabled. | | Update credential | Fields merge individually: `{"body": false}` disables body substitution and leaves `header` unchanged. | A credential must have at least one location enabled, so a create or update that would disable both locations returns a 400 error. Passing an explicit `null` for the `injection_location` object or for either field also returns a 400 error ("omit the field instead"). The response always returns both fields with their resolved values. A placeholder in a disabled location is neither substituted nor stripped. The request is sent to the third party with the literal opaque placeholder string in that location. If a request arrives at the third party containing the literal placeholder string, either that location is disabled for the credential or the destination host is not covered by the credential's `networking.allowed_hosts`. Credentials created in the Console enable header injection only. If your client sends the secret in the request body, such as a form-encoded token request, the placeholder passes through literally and the service rejects it with its own authentication error. Enable body injection in the Console form when you create the credential, or update the credential with `{"injection_location": {"body": true}}`. The substitution happens at egress, not inside the sandbox. Anything that processes the credential locally sees the opaque placeholder, not the real value: clients that validate the credential format at startup may reject it, and clients that compute a request signature from the secret (for example, AWS SigV4) produce an invalid signature. Environment variable credentials work for clients that send the secret value verbatim in an outbound request, in a location the credential's `injection_location` enables. Substitution is outbound only. If a client uses the stored secret to fetch a session token (for example, an OAuth client-credentials grant), the returned token arrives in the sandbox unredacted. For exchange-based flows, perform the exchange yourself and store the resulting token in the vault instead. Scope the API key to only the permissions the agent needs. The agent can do anything the key allows, so a key with broader permissions than necessary increases the blast radius if the agent behaves unexpectedly. Credentials are stored as provided and are not validated until session runtime. An invalid credential surfaces as an authentication or downstream error during the session, which is emitted but does not block the session from continuing. Constraints: * **Unique key per vault.** `mcp_server_url` (MCP credentials) and `secret_name` (environment variable credentials) must be unique among active credentials in a vault. Creating a duplicate returns a 409. * **Keys are immutable.** To change `mcp_server_url` or `secret_name`, archive the credential and create a new one. * **Maximum 20 credentials per vault.** ## Reference the vault at session creation Pass `vault_ids` when creating a session: ```bash cURL session_id=$(curl --fail-with-body -sS https://api.anthropic.com/v1/sessions \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ --data @- <beta->sessions->create( agent: $agent->id, environmentID: $environment->id, vaultIDs: [$vault->id], title: "Alice's Slack digest", ); ``` ```ruby Ruby session = client.beta.sessions.create( agent: agent.id, environment_id: environment.id, vault_ids: [vault.id], title: "Alice's Slack digest" ) ``` Runtime behavior: * When no MCP credential matches by `mcp_server_url`, the connection is attempted unauthenticated and will error if the server requires authentication. * When multiple vaults contain a matching credential, the first vault with a match wins. * In [multiagent sessions](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration), vault credentials apply to every thread. An agent whose own definition declares the matching MCP server authenticates with these credentials. See [Connect agents to MCP servers](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration#connect-agents-to-mcp-servers). ## Rotate a credential Secret values, `display_name`, and (on environment variable credentials) `injection_location` can be updated. `injection_location` updates merge per field, as described in the Environment variable tab of [Add a credential](https://platform.claude.com/docs/en/managed-agents/vaults#add-a-credential). For a running session, an `injection_location` update propagates the same way as a secret rotation: the session's credentials are re-resolved without a restart, as described in [Credential lifecycle](https://platform.claude.com/docs/en/managed-agents/vaults#credential-lifecycle), and the updated locations apply to the session's subsequent outbound requests. Structural fields (`mcp_server_url`, `secret_name`, `token_endpoint`, `client_id`) are locked after creation. To change them, archive the credential and create a new one. ```bash cURL curl --fail-with-body -sS \ "https://api.anthropic.com/v1/vaults/$vault_id/credentials/$credential_id" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ --data @- <<'EOF' > /dev/null { "auth": { "type": "mcp_oauth", "access_token": "xoxp-new-...", "expires_at": "2099-12-31T23:59:59Z", "refresh": {"refresh_token": "xoxe-1-new-..."} } } EOF ``` ```bash CLI ant beta:vaults:credentials update \ --vault-id "$VAULT_ID" \ --credential-id "$CREDENTIAL_ID" <<'YAML' auth: type: mcp_oauth access_token: xoxp-new-... expires_at: "2099-12-31T23:59:59Z" refresh: refresh_token: xoxe-1-new-... YAML ``` ```python Python client.beta.vaults.credentials.update( credential.id, vault_id=vault.id, auth={ "type": "mcp_oauth", "access_token": "xoxp-new-...", "expires_at": "2099-12-31T23:59:59Z", "refresh": {"refresh_token": "xoxe-1-new-..."}, }, ) ``` ```typescript TypeScript await client.beta.vaults.credentials.update(credential.id, { vault_id: vault.id, auth: { type: "mcp_oauth", access_token: "xoxp-new-...", expires_at: "2099-12-31T23:59:59Z", refresh: { refresh_token: "xoxe-1-new-...", }, }, }); ``` ```csharp C# await client.Beta.Vaults.Credentials.Update(credential.ID, new() { VaultID = vault.ID, Auth = new BetaManagedAgentsMcpOAuthUpdateParams { Type = BetaManagedAgentsMcpOAuthUpdateParamsType.McpOAuth, AccessToken = "xoxp-new-...", ExpiresAt = DateTimeOffset.Parse("2099-12-31T23:59:59Z"), Refresh = new() { RefreshToken = "xoxe-1-new-..." }, }, }); ``` ```go Go _, err = client.Beta.Vaults.Credentials.Update(ctx, credential.ID, anthropic.BetaVaultCredentialUpdateParams{ VaultID: vault.ID, Auth: anthropic.BetaVaultCredentialUpdateParamsAuthUnion{ OfMCPOAuth: &anthropic.BetaManagedAgentsMCPOAuthUpdateParams{ Type: anthropic.BetaManagedAgentsMCPOAuthUpdateParamsTypeMCPOAuth, AccessToken: anthropic.String("xoxp-new-..."), ExpiresAt: anthropic.Time(time.Date(2099, time.December, 31, 23, 59, 59, 0, time.UTC)), Refresh: anthropic.BetaManagedAgentsMCPOAuthRefreshUpdateParams{ RefreshToken: anthropic.String("xoxe-1-new-..."), }, }, }, }) if err != nil { panic(err) } ``` ```java Java client.beta().vaults().credentials().update(credential.id(), CredentialUpdateParams.builder() .vaultId(vault.id()) .auth(BetaManagedAgentsMcpOAuthUpdateParams.builder() .type(BetaManagedAgentsMcpOAuthUpdateParams.Type.MCP_OAUTH) .accessToken("xoxp-new-...") .expiresAt(OffsetDateTime.parse("2099-12-31T23:59:59Z")) .refresh(BetaManagedAgentsMcpOAuthRefreshUpdateParams.builder() .refreshToken("xoxe-1-new-...") .build()) .build()) .build()); ``` ```php PHP $client->beta->vaults->credentials->update( $credential->id, vaultID: $vault->id, auth: ManagedAgentsMCPOAuthUpdateParams::with( type: 'mcp_oauth', accessToken: 'xoxp-new-...', expiresAt: new DateTimeImmutable('2099-12-31T23:59:59Z'), refresh: ManagedAgentsMCPOAuthRefreshUpdateParams::with(refreshToken: 'xoxe-1-new-...'), ), ); ``` ```ruby Ruby client.beta.vaults.credentials.update( credential.id, vault_id: vault.id, auth: { type: "mcp_oauth", access_token: "xoxp-new-...", expires_at: "2099-12-31T23:59:59Z", refresh: {refresh_token: "xoxe-1-new-..."} } ) ``` ## Credential lifecycle Credentials are re-resolved periodically, both during a session and during the vault lifecycle. This ensures that credential rotation, archival, or deletion propagates to running sessions without a restart. To be notified if a credential is archived, deleted, or fails to refresh, you can subscribe to the vault and credential [webhooks](https://platform.claude.com/docs/en/managed-agents/webhooks) associated with those lifecycle changes. | Event | Trigger | | --------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | `vault.archived` | Vault archived. A `vault_credential.archived` event is also emitted for each underlying credential. | | `vault.deleted` | Vault deleted. A `vault_credential.deleted` event is also emitted for each underlying credential. | | `vault_credential.archived` | Credential archived, either directly or as a result of vault archival. | | `vault_credential.deleted` | Credential deleted, either directly or as a result of vault deletion. | | `vault_credential.refresh_failed` | An `mcp_oauth` credential cannot be refreshed (invalid refresh token, or irrecoverable error from the OAuth server). | This is a non-exhaustive list of webhooks; see [Subscribe to webhooks](https://platform.claude.com/docs/en/managed-agents/webhooks) for the complete list. For `mcp_oauth` credentials, re-resolution also refreshes the access token if it has expired. If the refresh fails, a `vault_credential.refresh_failed` event is emitted. ### Diagnose an OAuth refresh failure To diagnose why a refresh failed, call `POST /v1/vaults/{vault_id}/credentials/{credential_id}/mcp_oauth_validate` (or `client.beta.vaults.credentials.mcp_oauth_validate(...)` in the SDK). This lets you decide how to handle the failure; the right action depends on the error type. The top-level `status` tells you what to do next: * `valid`: the token works; no action needed. * `invalid`: the grant is gone or the OAuth server rejected the refresh with a 4xx. Prompt the end user to re-authorize. * `unknown`: a transient error (5xx, 429, or network failure). Wait and retry. ```bash cURL curl --fail-with-body -sS -X POST \ "https://api.anthropic.com/v1/vaults/$vault_id/credentials/$credential_id/mcp_oauth_validate?beta=true" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" ``` ```bash CLI ant beta:vaults:credentials mcp-oauth-validate \ --vault-id "$VAULT_ID" \ --credential-id "$CREDENTIAL_ID" \ --transform status --raw-output # "valid", "invalid", or "unknown" ``` ```python Python validation = client.beta.vaults.credentials.mcp_oauth_validate( credential.id, vault_id=vault.id, ) print(validation.status) # "valid", "invalid", or "unknown" ``` ```typescript TypeScript const validation = await client.beta.vaults.credentials.mcpOAuthValidate( credential.id, { vault_id: vault.id }, ); console.log(validation.status); // "valid", "invalid", or "unknown" ``` ```csharp C# var validation = await client.Beta.Vaults.Credentials.McpOAuthValidate(credential.ID, new() { VaultID = vault.ID, }); Console.WriteLine(validation.Status.Raw()); // "valid", "invalid", or "unknown" ``` ```go Go validation, err := client.Beta.Vaults.Credentials.MCPOAuthValidate(ctx, credential.ID, anthropic.BetaVaultCredentialMCPOAuthValidateParams{ VaultID: vault.ID, }) if err != nil { panic(err) } fmt.Println(validation.Status) // "valid", "invalid", or "unknown" ``` ```java Java var validation = client.beta().vaults().credentials().mcpOAuthValidate(credential.id(), CredentialMcpOAuthValidateParams.builder() .vaultId(vault.id()) .build()); IO.println(validation.status()); // valid, invalid, or unknown ``` ```php PHP $validation = $client->beta->vaults->credentials->mcpOAuthValidate( $credential->id, vaultID: $vault->id, ); echo $validation->status . "\n"; // "valid", "invalid", or "unknown" ``` ```ruby Ruby validation = client.beta.vaults.credentials.mcp_oauth_validate( credential.id, vault_id: vault.id ) puts validation.status # :valid, :invalid, or :unknown ``` The response is a `vault_credential_validation` object. `mcp_probe` includes the failed MCP handshake step; `refresh` includes the outcome of the attempted refresh. ```json { "type": "vault_credential_validation", "credential_id": "vcrd_01ABC...", "vault_id": "vlt_01XYZ...", "validated_at": "2026-04-29T17:12:00Z", "has_refresh_token": false, "status": "invalid", "mcp_probe": { "method": "initialize", "http_response": { "status_code": 401, "content_type": "application/json", "body": "{\"error\":\"invalid_token\"}", "body_truncated": false } }, "refresh": { "status": "no_refresh_token", "http_response": null } } ``` ## Other operations * **List vaults or credentials:** Paginated, newest first. Archived records are excluded by default (pass `include_archived=true` to include them). * **Archive a vault:** `POST /v1/vaults/{id}/archive`. Cascades to all credentials. Secrets are purged; records are retained for auditing. Future sessions referencing this vault fail; running sessions continue. * **Archive a credential:** `POST /v1/vaults/{id}/credentials/{cred_id}/archive`. Purges the secret payload; the credential key (`mcp_server_url` or `secret_name`) remains visible and is freed for a replacement credential. * **Delete a vault or credential:** Hard delete. The record is not retained. Use archive if you need an audit trail. --- title: Define outcomes url: https://platform.claude.com/docs/en/managed-agents/define-outcomes description: Tell the agent what 'done' looks like, and let it iterate until it gets there. --- An outcome tells the session what the end result should look like and how to measure its quality. The agent works toward that target, self-evaluating and iterating until the outcome is met. When you define an outcome, the harness automatically provisions a *grader* to evaluate the artifact against a rubric. The grader uses a separate context window to avoid being influenced by the main agent's implementation choices. The grader returns an explanation summarizing which criteria passed or failed, or confirming that the artifact satisfies the rubric. That feedback is handed back to the agent for the next iteration. Managed Agents API requests require the `managed-agents-2026-04-01` beta header, except memory store endpoints, which use `agent-memory-2026-07-22` instead. The SDK sets the correct beta header automatically. See [Beta headers](https://platform.claude.com/docs/en/api/beta-headers#endpoint-specific-headers). ## Create a rubric A rubric is a markdown document describing per-criterion scoring. The rubric is required. Structure the rubric as explicit, gradeable criteria, such as "The CSV contains a price column with numeric values" rather than "The data looks good." The grader scores each criterion independently, so vague criteria produce noisy evaluations. If you don't have a rubric on hand, try giving Claude an example of a known-good artifact and asking it to analyze what makes that content good, then turn that analysis into a rubric. This middle-ground approach often produces better results than writing criteria from scratch. Example rubric: ```markdown # DCF Model Rubric ## Revenue Projections - Uses historical revenue data from the last 5 fiscal years - Projects revenue for at least 5 years forward - Growth rate assumptions are explicitly stated and reasonable ## Cost Structure - COGS and operating expenses are modeled separately - Margins are consistent with historical trends or deviations are justified ## Discount Rate - WACC is calculated with stated assumptions for cost of equity and cost of debt - Beta, risk-free rate, and equity risk premium are sourced or justified ## Terminal Value - Uses either perpetuity growth or exit multiple method (stated which) - Terminal growth rate does not exceed long-term GDP growth ## Output Quality - All figures are in a single .xlsx file with clearly labeled sheets - Key assumptions are on a separate "Assumptions" sheet - Sensitivity analysis on WACC and terminal growth rate is included ``` Pass the rubric as inline text on `user.define_outcome` (see [Create a session with an outcome](https://platform.claude.com/docs/en/managed-agents/define-outcomes#create-a-session-with-an-outcome)), or upload it through the Files API for reuse across sessions. Uploading through the Files API requires a beta header that grants Files API access. Your Managed Agents beta header grants this on its own, so you don't need to send `files-api-2025-04-14` alongside it. The curl example passes its headers explicitly. ```bash cURL rubric=$(curl -fsSL https://api.anthropic.com/v1/files \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -F file=@/tmp/rubric.md) rubric_id=$(jq -r '.id' <<<"$rubric") printf 'Uploaded rubric: %s\n' "$rubric_id" ``` ```bash CLI RUBRIC_ID=$(ant beta:files upload \ --file /tmp/rubric.md \ --transform id --raw-output) ``` ```python Python import time from pathlib import Path from anthropic import Anthropic client = Anthropic() RUBRIC = """# DCF Model Rubric ## Revenue Projections - Uses historical revenue data from the last 5 fiscal years - Projects revenue for at least 5 years forward ## Output Quality - All figures are in a single .xlsx file with clearly labeled sheets """ Path("/tmp/rubric.md").write_text(RUBRIC) rubric = client.beta.files.upload(file=Path("/tmp/rubric.md")) print(f"Uploaded rubric: {rubric.id}") ``` ```typescript TypeScript import { writeFile, readFile } from "node:fs/promises"; import Anthropic from "@anthropic-ai/sdk"; import { toFile } from "@anthropic-ai/sdk"; const client = new Anthropic(); const RUBRIC = `# DCF Model Rubric ## Revenue Projections - Uses historical revenue data from the last 5 fiscal years - Projects revenue for at least 5 years forward ## Output Quality - All figures are in a single .xlsx file with clearly labeled sheets `; await writeFile("/tmp/rubric.md", RUBRIC); const rubric = await client.beta.files.upload({ file: await toFile(readFile("/tmp/rubric.md"), "/tmp/rubric.md"), }); console.log(`Uploaded rubric: ${rubric.id}`); ``` ```csharp C# using Anthropic; using Anthropic.Models.Beta.Agents; using Anthropic.Models.Beta.Environments; using Anthropic.Models.Beta.Files; using Anthropic.Models.Beta.Sessions; using Anthropic.Models.Beta.Sessions.Events; var client = new AnthropicClient(); const string Rubric = """ # DCF Model Rubric ## Revenue Projections - Uses historical revenue data from the last 5 fiscal years - Projects revenue for at least 5 years forward ## Output Quality - All figures are in a single .xlsx file with clearly labeled sheets """; await File.WriteAllTextAsync("/tmp/rubric.md", Rubric); var rubric = await client.Beta.Files.Upload(new() { File = File.OpenRead("/tmp/rubric.md"), }); Console.WriteLine($"Uploaded rubric: {rubric.ID}"); ``` ```go Go package main import ( "context" "fmt" "io" "os" "time" "github.com/anthropics/anthropic-sdk-go" ) const rubric = `# DCF Model Rubric ## Revenue Projections - Uses historical revenue data from the last 5 fiscal years - Projects revenue for at least 5 years forward ## Output Quality - All figures are in a single .xlsx file with clearly labeled sheets ` func main() { ctx := context.Background() client := anthropic.NewClient() if err := os.WriteFile("/tmp/rubric.md", []byte(rubric), 0o644); err != nil { panic(err) } f, err := os.Open("/tmp/rubric.md") if err != nil { panic(err) } uploaded, err := client.Beta.Files.Upload(ctx, anthropic.BetaFileUploadParams{ File: anthropic.File(f, "rubric.md", "text/markdown"), }) if err != nil { panic(err) } fmt.Printf("Uploaded rubric: %s\n", uploaded.ID) ``` ```java Java import com.anthropic.client.okhttp.AnthropicOkHttpClient; import com.anthropic.core.http.HttpResponse; import com.anthropic.models.beta.AnthropicBeta; import com.anthropic.models.beta.agents.AgentCreateParams; import com.anthropic.models.beta.agents.BetaManagedAgentsAgentToolset20260401Params; import com.anthropic.models.beta.agents.BetaManagedAgentsModel; import com.anthropic.models.beta.environments.BetaCloudConfigParams; import com.anthropic.models.beta.environments.EnvironmentCreateParams; import com.anthropic.models.beta.files.FileListParams; import com.anthropic.models.beta.files.FileUploadParams; import com.anthropic.models.beta.sessions.SessionCreateParams; import com.anthropic.models.beta.sessions.events.BetaManagedAgentsTextRubricParams; import com.anthropic.models.beta.sessions.events.BetaManagedAgentsUserDefineOutcomeEventParams; import com.anthropic.models.beta.sessions.events.BetaManagedAgentsUserInterruptEventParams; import com.anthropic.models.beta.sessions.events.EventSendParams; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; void main() throws Exception { var client = AnthropicOkHttpClient.fromEnv(); var RUBRIC = """ # DCF Model Rubric ## Revenue Projections - Uses historical revenue data from the last 5 fiscal years - Projects revenue for at least 5 years forward ## Output Quality - All figures are in a single .xlsx file with clearly labeled sheets """; Files.writeString(Path.of("/tmp/rubric.md"), RUBRIC); var rubric = client.beta().files().upload( FileUploadParams.builder() .file(Path.of("/tmp/rubric.md")) .build()); IO.println("Uploaded rubric: " + rubric.id()); ``` ```php PHP use Anthropic\Client; use Anthropic\Core\FileParam; $client = new Client(); $rubricText = <<<'MD' # DCF Model Rubric ## Revenue Projections - Uses historical revenue data from the last 5 fiscal years - Projects revenue for at least 5 years forward ## Output Quality - All figures are in a single .xlsx file with clearly labeled sheets MD; file_put_contents('/tmp/rubric.md', $rubricText); $rubric = $client->beta->files->upload( file: FileParam::fromResource(fopen('/tmp/rubric.md', 'r'), contentType: 'text/markdown'), ); echo "Uploaded rubric: {$rubric->id}\n"; ``` ```ruby Ruby require "anthropic" require "pathname" client = Anthropic::Client.new RUBRIC = <<~MD # DCF Model Rubric ## Revenue Projections - Uses historical revenue data from the last 5 fiscal years - Projects revenue for at least 5 years forward ## Output Quality - All figures are in a single .xlsx file with clearly labeled sheets MD File.write("/tmp/rubric.md", RUBRIC) rubric = client.beta.files.upload(file: Pathname.new("/tmp/rubric.md")) puts "Uploaded rubric: #{rubric.id}" ``` ## Create a session with an outcome The following examples create a [session](https://platform.claude.com/docs/en/managed-agents/sessions) for an existing [agent](https://platform.claude.com/docs/en/managed-agents/agent-setup) and [environment](https://platform.claude.com/docs/en/managed-agents/environments) (both created separately), then send a `user.define_outcome` event. The agent begins work immediately. No additional user message event is required. ```bash cURL # Create a session session=$(curl -fsSL https://api.anthropic.com/v1/sessions \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ --json @- </dev/null <beta->sessions->create( agent: $agent->id, environmentID: $environment->id, title: 'Financial analysis on Costco', ); // Define the outcome — agent starts working on receipt $client->beta->sessions->events->send( $session->id, events: [ [ 'type' => 'user.define_outcome', 'description' => 'Build a DCF model for Costco in .xlsx', 'rubric' => ['type' => 'text', 'content' => $rubricText], // or: 'rubric' => ['type' => 'file', 'file_id' => $rubric->id], 'max_iterations' => 5, // optional; default 3, max 20 ], ], ); ``` ```ruby Ruby # Create a session session = client.beta.sessions.create( agent: agent.id, environment_id: environment.id, title: "Financial analysis on Costco" ) # Define the outcome — agent starts working on receipt client.beta.sessions.events.send_( session.id, events: [ { type: "user.define_outcome", description: "Build a DCF model for Costco in .xlsx", rubric: {type: "text", content: RUBRIC}, # or: rubric: {type: "file", file_id: rubric.id}, max_iterations: 5 # optional; default 3, max 20 } ] ) ``` You can also define the outcome in the create request itself: pass a single `user.define_outcome` event in [`initial_events`](https://platform.claude.com/docs/en/managed-agents/sessions#seed-the-session-with-initial-events) to create the session and start work toward the outcome in one call. ## Outcome events Progress on an outcome-oriented session is surfaced on the events [stream](https://platform.claude.com/docs/en/managed-agents/events-and-streaming). * `agent.*` events (such as messages and tool use) show progress toward the outcome. * `span.outcome_evaluation_*` events are only emitted for outcome-oriented sessions and show the number of iteration loops and the grader's feedback process. * You can also send `user.message` [events](https://platform.claude.com/docs/en/managed-agents/reference#event-types) to an outcome-oriented session to direct the agent's work as it progresses, but it isn't required: the agent works toward the outcome on its own, iterating until it succeeds or runs out of iterations. * A `user.interrupt` event pauses work on the current outcome and marks the `span.outcome_evaluation_end.result` as `interrupted`, allowing you to kick off a new outcome. * After the final outcome evaluation, the session can be continued as a conversational session, or a new outcome can be started. The session retains history of the prior outcome. ### Define outcome user event Only one outcome is supported at a time, but you may chain outcomes in sequence. To do this, send a new `user.define_outcome` event after the terminal `span.outcome_evaluation_end` event of the previous outcome. This is the event you send to initiate an outcome. It is echoed back on receipt, including a `processed_at` timestamp and `outcome_id`. ```json { "type": "user.define_outcome", "description": "Build a DCF model for Costco in .xlsx", "rubric": { "type": "file", "file_id": "file_01..." }, "max_iterations": 5 } ``` ### Outcome evaluation start Emitted once the grader starts an evaluation over one iteration loop. The `iteration` field is a 0-indexed revision counter: `0` is the first evaluation, `1` is the re-evaluation after the first revision, and so on. ```json { "type": "span.outcome_evaluation_start", "id": "sevt_01def...", "outcome_id": "outc_01a...", "iteration": 0, "processed_at": "2026-03-25T14:01:45Z" } ``` ### Outcome evaluation ongoing Heartbeat emitted while the grader runs. The grader's internal reasoning is opaque: you see that it's working, not what it's thinking. ```json { "type": "span.outcome_evaluation_ongoing", "id": "sevt_01ghi...", "outcome_id": "outc_01a...", "iteration": 0, "processed_at": "2026-03-25T14:02:10Z" } ``` ### Outcome evaluation end Emitted when an outcome evaluation cycle ends: after the grader finishes evaluating one iteration, or when the session is interrupted while an outcome is active. The `result` field indicates what happens next. | Result | Next | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `satisfied` | Session transitions to `idle`. | | `needs_revision` | Agent starts a new iteration cycle. | | `max_iterations_reached` | One final acknowledgment turn follows before the session transitions to `idle`. No further evaluation runs. | | `failed` | Session transitions to `idle`. Returned when the rubric does not apply to the deliverables, for example if the description and rubric contradict each other. | | `interrupted` | Emitted when the session is interrupted while an outcome is active, even if evaluation hadn't started yet. If no `outcome_evaluation_start` fired before the interrupt, `outcome_evaluation_start_id` is an empty string. | ```json { "type": "span.outcome_evaluation_end", "id": "sevt_01jkl...", "outcome_evaluation_start_id": "sevt_01def...", "outcome_id": "outc_01a...", "result": "satisfied", "explanation": "All 12 criteria met: revenue projections use 5 years of historical data, WACC assumptions are stated, sensitivity table is included...", "iteration": 0, "usage": { "input_tokens": 2400, "output_tokens": 350, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 1800 }, "processed_at": "2026-03-25T14:03:00Z" } ``` ## Check outcome status You can either listen on the [event stream](https://platform.claude.com/docs/en/managed-agents/events-and-streaming) for `span.outcome_evaluation_end`, or poll `GET /v1/sessions/{session_id}` and read `outcome_evaluations[].result`. Until an evaluation completes, `result` reports `pending`, `running`, or `evaluating`: ```bash cURL session=$(curl -fsSL "https://api.anthropic.com/v1/sessions/$session_id" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01") jq -r '.outcome_evaluations[] | "\(.outcome_id): \(.result)"' <<<"$session" # outc_01a...: satisfied ``` ```bash CLI ant beta:sessions retrieve --session-id "$SESSION_ID" \ --transform 'outcome_evaluations' --format yaml ``` ```python Python session = client.beta.sessions.retrieve(session.id) for outcome in session.outcome_evaluations: print(f"{outcome.outcome_id}: {outcome.result}") # outc_01a...: satisfied ``` ```typescript TypeScript const retrieved = await client.beta.sessions.retrieve(session.id); for (const outcome of retrieved.outcome_evaluations) { console.log(`${outcome.outcome_id}: ${outcome.result}`); // outc_01a...: satisfied } ``` ```csharp C# session = await client.Beta.Sessions.Retrieve(session.ID); foreach (var outcome in session.OutcomeEvaluations) { Console.WriteLine($"{outcome.OutcomeID}: {outcome.Result}"); // outc_01a...: satisfied } ``` ```go Go session, err = client.Beta.Sessions.Get(ctx, session.ID, anthropic.BetaSessionGetParams{}) if err != nil { panic(err) } for _, outcome := range session.OutcomeEvaluations { fmt.Printf("%s: %s\n", outcome.OutcomeID, outcome.Result) // outc_01a...: satisfied } ``` ```java Java var retrieved = client.beta().sessions().retrieve(session.id()); for (var outcome : retrieved.outcomeEvaluations()) { IO.println(outcome.outcomeId() + ": " + outcome.result()); // outc_01a...: satisfied } ``` ```php PHP $session = $client->beta->sessions->retrieve($session->id); foreach ($session->outcomeEvaluations as $outcome) { echo "{$outcome->outcomeID}: {$outcome->result}\n"; // outc_01a...: satisfied } ``` ```ruby Ruby session = client.beta.sessions.retrieve(session.id) session.outcome_evaluations.each do puts "#{it.outcome_id}: #{it.result}" # outc_01a...: satisfied end ``` ## Retrieve deliverables The agent writes output files to `/mnt/session/outputs/` inside the sandbox. Once the session is idle, fetch them through the [Files API](https://platform.claude.com/docs/en/build-with-claude/files) scoped to the session. Filtering by `scope_id` requires the `managed-agents-2026-04-01` beta header on the files request. The SDK files methods send only the files beta automatically, so the examples pass it explicitly. ```bash cURL # List files produced by this session # scope_id filtering requires the managed-agents beta files=$(curl -fsSL "https://api.anthropic.com/v1/files?scope_id=$session_id" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01") jq -r '.data[] | "\(.id) \(.filename)"' <<<"$files" # Download a file file_id=$(jq -r '.data[0].id // empty' <<<"$files") if [[ -n $file_id ]]; then curl -fsSL "https://api.anthropic.com/v1/files/$file_id/content" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -o /tmp/output.txt fi ``` ```bash CLI # List files produced by this session # scope_id filtering requires the managed-agents beta on the files request ant beta:files list --scope-id "$SESSION_ID" \ --beta managed-agents-2026-04-01 # Download a file FILE_ID=$(ant beta:files list --scope-id "$SESSION_ID" \ --beta managed-agents-2026-04-01 \ --transform 'data[0].id' --raw-output) if [[ -n $FILE_ID ]]; then ant beta:files download --file-id "$FILE_ID" --output /tmp/output.txt fi ``` ```python Python # List files produced by this session # scope_id filtering requires the managed-agents beta on the files request files = client.beta.files.list(scope_id=session.id, betas=["managed-agents-2026-04-01"]) for file in files: print(file.id, file.filename) # Download a file if files.data: content = client.beta.files.download(files.data[0].id) content.write_to_file("/tmp/output.txt") ``` ```typescript TypeScript // List files produced by this session // scope_id filtering requires the managed-agents beta on the files request const files = await client.beta.files.list({ scope_id: session.id, betas: ["managed-agents-2026-04-01"], }); for (const file of files.data) { console.log(file.id, file.filename); } // Download a file if (files.data.length > 0) { const content = await client.beta.files.download(files.data[0].id); await writeFile("/tmp/output.txt", new Uint8Array(await content.arrayBuffer())); } ``` ```csharp C# // List files produced by this session // (scope_id filtering requires the managed-agents beta on the files request) var files = await client.Beta.Files.List(new() { ScopeID = session.ID, Betas = ["managed-agents-2026-04-01"], }); foreach (var file in files.Items) { Console.WriteLine($"{file.ID} {file.Filename}"); } // Download a file if (files.Items.Count > 0) { using var download = await client.Beta.Files.Download(files.Items[0].ID); await using var output = File.Create("/tmp/output.txt"); await (await download.ReadAsStream()).CopyToAsync(output); } ``` ```go Go // List files produced by this session // (scope_id filtering requires the managed-agents beta on the files request) files, err := client.Beta.Files.List(ctx, anthropic.BetaFileListParams{ ScopeID: anthropic.String(session.ID), Betas: []anthropic.AnthropicBeta{anthropic.AnthropicBetaManagedAgents2026_04_01}, }) if err != nil { panic(err) } for _, file := range files.Data { fmt.Println(file.ID, file.Filename) } // Download a file if len(files.Data) > 0 { resp, err := client.Beta.Files.Download(ctx, files.Data[0].ID, anthropic.BetaFileDownloadParams{}) if err != nil { panic(err) } defer resp.Body.Close() out, err := os.Create("/tmp/output.txt") if err != nil { panic(err) } defer out.Close() if _, err := io.Copy(out, resp.Body); err != nil { panic(err) } } ``` ```java Java // List files produced by this session // (scope_id filtering requires the managed-agents beta on the files request) var files = client.beta().files().list( FileListParams.builder() .scopeId(session.id()) .addBeta(AnthropicBeta.MANAGED_AGENTS_2026_04_01) .build()); for (var file : files.data()) { IO.println(file.id() + " " + file.filename()); } // Download a file if (!files.data().isEmpty()) { try (HttpResponse response = client.beta().files().download(files.data().getFirst().id())) { try (InputStream body = response.body()) { Files.copy(body, Path.of("/tmp/output.txt"), StandardCopyOption.REPLACE_EXISTING); } } } ``` ```php PHP // List files produced by this session // scope_id filtering requires the managed-agents beta on the files request $files = $client->beta->files->list(scopeID: $session->id, betas: ['managed-agents-2026-04-01']); foreach ($files->data as $file) { echo "{$file->id} {$file->filename}\n"; } // Download a file if (count($files->data) > 0) { $content = $client->beta->files->download($files->data[0]->id); file_put_contents('/tmp/output.txt', $content); } ``` ```ruby Ruby # List files produced by this session # scope_id filtering requires the managed-agents beta on the files request files = client.beta.files.list(scope_id: session.id, betas: ["managed-agents-2026-04-01"]) files.data.each { |file| puts "#{file.id} #{file.filename}" } # Download a file if (first = files.data.first) content = client.beta.files.download(first.id) File.binwrite("/tmp/output.txt", content.read) end ``` ## Next steps Register per-user credentials when creating sessions. Send events, stream responses, and interrupt or redirect your session mid-execution. Upload files and mount them in your sandbox for reading and processing. --- title: Session budgets url: https://platform.claude.com/docs/en/managed-agents/budgets description: Cap a session's spend with a hard dollar budget enforced at public list rates. --- A session budget is an optional hard spend ceiling you set when you [create a session](https://platform.claude.com/docs/en/managed-agents/sessions). The platform continuously prices everything the session consumes at public list rates (the session's **list cost**) and stops issuing new model requests once that cost reaches the budget. The request in flight when the cap is crossed still finishes, so the final list cost can land [a fraction past the budget](https://platform.claude.com/docs/en/managed-agents/budgets#when-a-session-reaches-its-budget). A session at its budget pauses and goes [idle](https://platform.claude.com/docs/en/managed-agents/session-operations#session-statuses) rather than terminating; changing or removing the budget resumes its work automatically. Deployments accept the same budget and apply it to each session they start; see [Budgets on deployments](https://platform.claude.com/docs/en/managed-agents/budgets#budgets-on-deployments). Managed Agents API requests require the `managed-agents-2026-04-01` beta header, except memory store endpoints, which use `agent-memory-2026-07-22` instead. The SDK sets the correct beta header automatically. See [Beta headers](https://platform.claude.com/docs/en/api/beta-headers#endpoint-specific-headers). ## Set a budget at session creation Pass the optional `budget` field when you create the session: ```bash cURL session=$(curl -sS --fail-with-body https://api.anthropic.com/v1/sessions \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d @- <beta->sessions->create( agent: $agent->id, environmentID: $environment->id, budget: [ 'type' => 'limit', 'max_list_cost' => ['amount' => '125', 'currency' => 'USD'], ], ); echo "{$session->id} {$session->budget->maxListCost->amount}\n"; // sesn_01... 125 ``` ```ruby Ruby session = client.beta.sessions.create( agent: agent.id, environment_id: environment.id, budget: { type: "limit", max_list_cost: {amount: "125", currency: "USD"} } ) puts "#{session.id} #{session.budget.max_list_cost.amount}" # sesn_01... 125 ``` The `budget` object has two fields: * `type` is always `"limit"`. * `max_list_cost` is the cap itself: `amount` is a whole number of US cents written as a string with no leading zeros (`"125"` is $1.25 and `"50"` is 50 cents) and must be greater than zero. Decimal forms such as `"25.00"` are rejected. The amount is a string rather than a number so no float rounding is ever applied to it. `currency` is an uppercase ISO-4217 currency code; `USD` is the only supported currency. A budget can only be attached when the session is created. Adding a budget to an existing session that doesn't have one is rejected with a 400 error. A budgeted session's cap can be [changed](https://platform.claude.com/docs/en/managed-agents/budgets#change-the-budget) or [removed](https://platform.claude.com/docs/en/managed-agents/budgets#remove-the-budget) at any time. ## How list cost is measured The platform prices what the session consumes, continuously, at public list rates: * **Model tokens**, at each served model's list price * **Web searches**, at $10 per 1,000 searches * **Session running time**, at $0.08 per hour This running dollar total is the session's **list cost**, and it is what the budget compares against. List cost is not your contracted price: if your organization has negotiated discounts, the session reaches its cap when the list-price total does, and your billed spend might be lower than the cap. Enforcement uses the exact, unrounded list cost. The `list_cost` figures reported on the session and its events are whole cents, rounded to the nearest cent, so a reported figure can read up to half a cent either side of the exact amount enforcement uses. ## When a session reaches its budget The cap is enforced between model requests, not mid-request. Before each model request, the platform checks the session's consumed list cost, and once that total reaches the cap every thread pauses before its next request. The request that carried the total past the cap was admitted while the session was still under it and runs to completion, so a paused session's recorded `list_cost` reads at or a fraction past `max_list_cost`: a session capped at `"50"` (50 cents) can pause with a `list_cost` of `"53"`. This is expected, not a billing error, and the overshoot is bounded by one model request per thread. Treat the budget as a bound on new work rather than an exact stopping point, and size the cap with that one-request margin in mind. A session that reaches its budget goes idle with a `stop_reason` of `budget_reached`; it is not terminated, and its history and sandbox are preserved like any other idle session's. On the [event stream](https://platform.claude.com/docs/en/managed-agents/events-and-streaming) you'll see, in order: 1. A `session.thread_status_idle` event with a `stop_reason` of `budget_reached` as each thread pauses. 2. A [`session.usage`](https://platform.claude.com/docs/en/managed-agents/budgets#monitor-spend) event with the session's cumulative usage and list cost. 3. A `session.status_idle` event with a `stop_reason` of `budget_reached`. The usage event always immediately precedes this idle event. A thread whose final request both crosses the cap and completes its turn reports `end_turn` on its own `session.thread_status_idle` event while the session still reports `budget_reached`; treat the session-level `stop_reason` as the signal that the session paused at its budget. ### Events accepted at the cap While the session is at or over its budget, it accepts only events that settle work already in progress: * `user.tool_confirmation` * `user.tool_result` * `user.custom_tool_result` * `user.interrupt` Any event that would start new work, such as `user.message`, is rejected with a 400 error naming this list. Settled results are recorded without triggering a new model request; the session stays paused at its budget. A `user.interrupt` sent while the session is paused at its budget (all threads paused at the cap) is accepted and ignored: it does not appear in the event list and changes nothing. Change or remove the budget to continue. ## Resume a session at its budget Change or remove the budget with a session update. An accepted update resumes the session's paused work automatically; no further client action is needed. ### Change the budget Update the session with a new `max_list_cost`. The new value can be higher or lower than the current cap, but it must be strictly greater than the session's consumed list cost; otherwise the update is rejected with a 400 error: `budget.max_list_cost must be greater than the session's consumed list cost`. Because the consumed cost usually sits [a fraction past the old cap](https://platform.claude.com/docs/en/managed-agents/budgets#when-a-session-reaches-its-budget) when the session pauses, base the new value on the session's reported `usage.list_cost`, not on the old `max_list_cost`. Set it a cent or more above that figure: the reported value is rounded and can sit a fraction below the exact consumed cost the check uses. ```bash cURL curl -sS --fail-with-body "https://api.anthropic.com/v1/sessions/$SESSION_ID" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d '{ "budget": { "type": "limit", "max_list_cost": {"amount": "500", "currency": "USD"} } }' ``` ```bash CLI ant beta:sessions update \ --session-id "$SESSION_ID" \ --budget '{type: limit, max_list_cost: {amount: "500", currency: USD}}' ``` ```python Python updated_session = client.beta.sessions.update( session.id, budget={ "type": "limit", "max_list_cost": {"amount": "500", "currency": "USD"}, }, ) print(updated_session.budget.max_list_cost.amount) # 500 ``` ```typescript TypeScript const updatedSession = await client.beta.sessions.update(session.id, { budget: { type: "limit", max_list_cost: { amount: "500", currency: "USD" } } }); console.log(updatedSession.budget?.max_list_cost.amount); // 500 ``` ```csharp C# var updatedSession = await client.Beta.Sessions.Update(session.ID, new() { Budget = new() { Type = BetaManagedAgentsBudgetLimitType.Limit, MaxListCost = new() { Amount = "500", Currency = BetaCurrency.Usd }, }, }); Console.WriteLine(updatedSession.Budget?.MaxListCost.Amount); // 500 ``` ```go Go updatedSession, err := client.Beta.Sessions.Update(ctx, session.ID, anthropic.BetaSessionUpdateParams{ Budget: anthropic.BetaManagedAgentsBudgetLimitParam{ Type: anthropic.BetaManagedAgentsBudgetLimitTypeLimit, MaxListCost: anthropic.BetaMonetaryAmountParam{ Amount: "500", Currency: anthropic.BetaCurrencyUsd, }, }, }) if err != nil { panic(err) } fmt.Println(updatedSession.Budget.MaxListCost.Amount) // 500 ``` ```java Java var updatedSession = client.beta().sessions().update(session.id(), SessionUpdateParams.builder() .budget(BetaManagedAgentsBudgetLimit.builder() .type(BetaManagedAgentsBudgetLimit.Type.LIMIT) .maxListCost(BetaMonetaryAmount.builder() .amount("500") .currency(BetaCurrency.USD) .build()) .build()) .build()); IO.println(updatedSession.budget().orElseThrow().maxListCost().amount()); // 500 ``` ```php PHP $updatedSession = $client->beta->sessions->update( $session->id, budget: [ 'type' => 'limit', 'max_list_cost' => ['amount' => '500', 'currency' => 'USD'], ], ); echo "{$updatedSession->budget->maxListCost->amount}\n"; // 500 ``` ```ruby Ruby updated_session = client.beta.sessions.update( session.id, budget: { type: "limit", max_list_cost: {amount: "500", currency: "USD"} } ) puts updated_session.budget.max_list_cost.amount # 500 ``` ### Remove the budget Set `budget` to `null` to remove the cap entirely. The session's paused work resumes, and the resulting `session.updated` event carries `budget` set to `null`. ```bash cURL curl -sS --fail-with-body "https://api.anthropic.com/v1/sessions/$SESSION_ID" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d '{"budget": null}' ``` ```bash CLI ant beta:sessions update --session-id "$SESSION_ID" --budget null ``` ```python Python unbudgeted_session = client.beta.sessions.update(session.id, budget=None) print(unbudgeted_session.budget) # None ``` ```typescript TypeScript const unbudgetedSession = await client.beta.sessions.update(session.id, { budget: null }); console.log(unbudgetedSession.budget); // null ``` ```csharp C# // Assigning null sends an explicit null; leaving Budget unset would omit the field. var unbudgetedSession = await client.Beta.Sessions.Update(session.ID, new() { Budget = null }); Console.WriteLine(unbudgetedSession.Budget is null); // True: the session no longer has a budget ``` ```go Go // A zero-value Budget is omitted from the request; param.NullStruct (from // github.com/anthropics/anthropic-sdk-go/packages/param) sends an explicit null. unbudgetedSession, err := client.Beta.Sessions.Update(ctx, session.ID, anthropic.BetaSessionUpdateParams{ Budget: param.NullStruct[anthropic.BetaManagedAgentsBudgetLimitParam](), }) if err != nil { panic(err) } fmt.Println(unbudgetedSession.JSON.Budget.Valid()) // false: the session no longer has a budget ``` ```java Java // An empty Optional sends an explicit null; leaving budget unset would omit the field. var unbudgetedSession = client.beta().sessions().update(session.id(), SessionUpdateParams.builder() .budget(Optional.empty()) .build()); IO.println(unbudgetedSession.budget().isPresent()); // false: the session no longer has a budget ``` ```php PHP // update(budget: null) omits the field, so send the explicit null through the raw client. $unbudgetedSession = $client->beta->sessions->raw ->update($session->id, ['budget' => null]) ->parse(); echo json_encode($unbudgetedSession->budget), "\n"; // null ``` ```ruby Ruby unbudgeted_session = client.beta.sessions.update(session.id, budget: nil) p unbudgeted_session.budget # nil ``` Removing a session's budget is one-way: a session whose budget has been removed cannot be given a new one. To keep a cap on the session, change the budget instead. ## Monitor spend The session object carries its `budget` and a `usage` object with the tracked spend: `usage.list_cost` is the session's consumed list cost, and `usage.active_seconds` is the running time its runtime cost is priced on. On a session paused at `budget_reached`, expect `usage.list_cost` to read at or a fraction past `max_list_cost`: the [request that crossed the cap](https://platform.claude.com/docs/en/managed-agents/budgets#when-a-session-reaches-its-budget) finished before the pause. Session-level `active_seconds` counts overlapping activity from concurrent threads once. Thread retrieval responses carry the same two fields on the thread's own `usage`, priced per thread. Per-thread figures are rounded independently and exclude the session's running-time cost, so they don't sum exactly to the session's `list_cost`; the session figure is the one the budget is enforced against. The `session.usage` event is a snapshot of the session's cumulative usage and tracked list cost. It carries the session's token totals, `list_cost`, `active_seconds`, `server_tool_use` request counts (`web_search_requests`, priced into list cost per request, and `web_fetch_requests`, which reads `0` because web fetch requests carry no per-request charge and aren't metered), and an echo of the session's `budget`, or `null` when the session has none. It appears in the events list and the session stream. The session emits one immediately before it goes idle, whatever the stop reason, so a session that reaches its budget always emits one immediately before the budget-reached idle event. For reading usage from the stream and the session object, see [Tracking usage](https://platform.claude.com/docs/en/managed-agents/events-and-streaming#tracking-usage). ## Budgets in multiagent sessions A [multiagent](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration) session has a single budget shared across all of its threads; there are no per-thread caps. Each thread's consumption is priced at its own served model, and threads pause independently as the shared cap is reached. [Advisor](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration#give-the-session-an-advisor) consultations count against the same budget, priced at the advisor model's rates. One thread can pause at `budget_reached` while another finishes its in-flight request. A pending ask outranks the cap: a session with one thread waiting on `requires_action` and another paused at `budget_reached` reports `requires_action` at the session level. The pending request still needs an answer, and answering it is a [settle event](https://platform.claude.com/docs/en/managed-agents/budgets#events-accepted-at-the-cap) the budget doesn't block. ## Budgets on deployments A [deployment](https://platform.claude.com/docs/en/managed-agents/scheduled-deployments) accepts the same `budget` object when you create or update it: ```json { "budget": { "type": "limit", "max_list_cost": { "amount": "2000", "currency": "USD" } } } ``` The cap is copied onto each session the deployment starts, so it bounds each run separately rather than the deployment's cumulative spend. Changing the deployment's budget applies to sessions the deployment starts afterward, not to sessions already running. Unlike a session, a deployment's budget can be cleared with `null` and set again later. See [Set a budget on each run](https://platform.claude.com/docs/en/managed-agents/scheduled-deployments#set-a-budget-on-each-run). ## Models without a list price A budget can only track consumption the platform can price. Creating a budgeted session whose agent, or any agent or advisor on its [multiagent roster](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration), uses a model with no public list price is rejected with a 400 error stating that no list price is available for the model. If a budgeted session's usage comes to include a model with no list price, the budget can no longer measure the session's spend: the session can pause with a `stop_reason` of `budget_reached`, and changing the budget is rejected. Remove the budget to resume the session. ## Error reference Budget-related requests are rejected in the following cases: | Condition | Status | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | | A work-starting event (for example, `user.message`) is sent while the session is at or over its budget; the error names the [accepted settle events](https://platform.claude.com/docs/en/managed-agents/budgets#events-accepted-at-the-cap) | 400 | | The budget is set to a value at or below the session's consumed list cost | 400 | | A budget is added to a session created without one, or re-added after removal | 400 | | `amount` is not a whole number of cents (for example, `"25.00"`), is zero or negative, or `currency` is not `USD` | 400 | | A budgeted create references a model with [no public list price](https://platform.claude.com/docs/en/managed-agents/budgets#models-without-a-list-price) | 400 | Session budgets are hard caps in US dollars (written in cents) on a single session, enforced by the platform. They are distinct from the Messages API's [task budgets](https://platform.claude.com/docs/en/build-with-claude/task-budgets), which are advisory, token-denominated budgets the model uses to self-regulate within one agentic loop. --- title: Session event stream url: https://platform.claude.com/docs/en/managed-agents/events-and-streaming description: Send events, stream responses, and interrupt or redirect your session mid-execution. --- Communication with Claude Managed Agents is event-based. You send user events to the agent, and receive agent and session events back to track status. Managed Agents API requests require the `managed-agents-2026-04-01` beta header, except memory store endpoints, which use `agent-memory-2026-07-22` instead. The SDK sets the correct beta header automatically. See [Beta headers](https://platform.claude.com/docs/en/api/beta-headers#endpoint-specific-headers). ## Event types Events flow in two directions. * **User events** and **system events** are what you send to the agent: `user.*` events start a session and steer it as it progresses; `system.message` appends system-level context that applies to the accompanying turn and all subsequent turns. * **Session events**, **span events**, and **agent events** are sent to you for observability into your session state and agent progress. Stream connections that opt in also receive [event deltas](https://platform.claude.com/docs/en/managed-agents/events-and-streaming#event-deltas). Session, span, agent, user, and system event type strings follow a `{domain}.{action}` naming convention. The stream-only delta preview events (`event_start`, `event_delta`) are the exception. See [Event types](https://platform.claude.com/docs/en/managed-agents/reference#event-types) in the reference for the full catalog. Every persisted event includes a `processed_at` timestamp set when the event finishes processing. On events you send, `processed_at` is null while the event is still queued behind earlier events. The exceptions are `user.define_outcome`, `user.custom_tool_result`, and `user.tool_result`, which are processed on receipt and echoed back with `processed_at` already populated. ## Integrating events Send a `user.message` event to start or continue the agent's work: ```bash cURL curl --fail-with-body -sS "https://api.anthropic.com/v1/sessions/$SESSION_ID/events?beta=true" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d @- <<'EOF' { "events": [ { "type": "user.message", "content": [ {"type": "text", "text": "Analyze the performance of the sort function in utils.py"} ] } ] } EOF ``` ```bash CLI ant beta:sessions:events send --session-id "$SESSION_ID" <<'YAML' events: - type: user.message content: - type: text text: Analyze the performance of the sort function in utils.py YAML ``` ```python Python client.beta.sessions.events.send( session.id, events=[ { "type": "user.message", "content": [ { "type": "text", "text": "Analyze the performance of the sort function in utils.py", }, ], }, ], ) ``` ```typescript TypeScript await client.beta.sessions.events.send(session.id, { events: [ { type: "user.message", content: [ { type: "text", text: "Analyze the performance of the sort function in utils.py", }, ], }, ], }); ``` ```csharp C# await client.Beta.Sessions.Events.Send(session.ID, new() { Events = [ new BetaManagedAgentsUserMessageEventParams { Type = BetaManagedAgentsUserMessageEventParamsType.UserMessage, Content = [ new BetaManagedAgentsTextBlock { Type = BetaManagedAgentsTextBlockType.Text, Text = "Analyze the performance of the sort function in utils.py", }, ], }, ], }); ``` ```go Go if _, err := client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{ Events: []anthropic.BetaManagedAgentsEventParamsUnion{{ OfUserMessage: &anthropic.BetaManagedAgentsUserMessageEventParams{ Type: anthropic.BetaManagedAgentsUserMessageEventParamsTypeUserMessage, Content: []anthropic.BetaManagedAgentsUserMessageEventParamsContentUnion{{ OfText: &anthropic.BetaManagedAgentsTextBlockParam{ Type: anthropic.BetaManagedAgentsTextBlockTypeText, Text: "Analyze the performance of the sort function in utils.py", }, }}, }, }}, }); err != nil { panic(err) } ``` ```java Java client.beta().sessions().events().send( session.id(), EventSendParams.builder() .addEvent(BetaManagedAgentsUserMessageEventParams.builder() .type(BetaManagedAgentsUserMessageEventParams.Type.USER_MESSAGE) .addTextContent("Analyze the performance of the sort function in utils.py") .build()) .build()); ``` ```php PHP $client->beta->sessions->events->send( $session->id, events: [ [ 'type' => 'user.message', 'content' => [ [ 'type' => 'text', 'text' => 'Analyze the performance of the sort function in utils.py', ], ], ], ], ); ``` ```ruby Ruby client.beta.sessions.events.send_( session.id, events: [ { type: "user.message", content: [ { type: "text", text: "Analyze the performance of the sort function in utils.py" } ] } ] ) ``` Send a `user.interrupt` event to stop the agent mid-execution, then follow up with a `user.message` event to redirect it: ```bash cURL # Agent is currently analyzing a file... # Interrupt with a new direction: curl --fail-with-body -sS "https://api.anthropic.com/v1/sessions/$SESSION_ID/events?beta=true" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d @- <<'EOF' { "events": [ {"type": "user.interrupt"}, { "type": "user.message", "content": [ {"type": "text", "text": "Instead, focus on fixing the bug in line 42."} ] } ] } EOF ``` ```bash CLI # Agent is currently analyzing a file... # Interrupt with a new direction: ant beta:sessions:events send --session-id "$SESSION_ID" <<'YAML' events: - type: user.interrupt - type: user.message content: - type: text text: Instead, focus on fixing the bug in line 42. YAML ``` ```python Python # Agent is currently analyzing a file... # Interrupt with a new direction: client.beta.sessions.events.send( session.id, events=[ {"type": "user.interrupt"}, { "type": "user.message", "content": [ { "type": "text", "text": "Instead, focus on fixing the bug in line 42.", }, ], }, ], ) ``` ```typescript TypeScript // Agent is currently analyzing a file... // Interrupt with a new direction: await client.beta.sessions.events.send(session.id, { events: [ { type: "user.interrupt" }, { type: "user.message", content: [ { type: "text", text: "Instead, focus on fixing the bug in line 42.", }, ], }, ], }); ``` ```csharp C# // Agent is currently analyzing a file... // Interrupt with a new direction: await client.Beta.Sessions.Events.Send(session.ID, new() { Events = [ new BetaManagedAgentsUserInterruptEventParams { Type = BetaManagedAgentsUserInterruptEventParamsType.UserInterrupt, }, new BetaManagedAgentsUserMessageEventParams { Type = BetaManagedAgentsUserMessageEventParamsType.UserMessage, Content = [ new BetaManagedAgentsTextBlock { Type = BetaManagedAgentsTextBlockType.Text, Text = "Instead, focus on fixing the bug in line 42.", }, ], }, ], }); ``` ```go Go // Agent is currently analyzing a file... // Interrupt with a new direction: if _, err := client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{ Events: []anthropic.BetaManagedAgentsEventParamsUnion{ { OfUserInterrupt: &anthropic.BetaManagedAgentsUserInterruptEventParams{ Type: anthropic.BetaManagedAgentsUserInterruptEventParamsTypeUserInterrupt, }, }, { OfUserMessage: &anthropic.BetaManagedAgentsUserMessageEventParams{ Type: anthropic.BetaManagedAgentsUserMessageEventParamsTypeUserMessage, Content: []anthropic.BetaManagedAgentsUserMessageEventParamsContentUnion{{ OfText: &anthropic.BetaManagedAgentsTextBlockParam{ Type: anthropic.BetaManagedAgentsTextBlockTypeText, Text: "Instead, focus on fixing the bug in line 42.", }, }}, }, }, }, }); err != nil { panic(err) } ``` ```java Java // Agent is currently analyzing a file... // Interrupt with a new direction: client.beta().sessions().events().send( session.id(), EventSendParams.builder() .addEvent(BetaManagedAgentsUserInterruptEventParams.builder() .type(BetaManagedAgentsUserInterruptEventParams.Type.USER_INTERRUPT) .build()) .addEvent(BetaManagedAgentsUserMessageEventParams.builder() .type(BetaManagedAgentsUserMessageEventParams.Type.USER_MESSAGE) .addTextContent("Instead, focus on fixing the bug in line 42.") .build()) .build()); ``` ```php PHP // Agent is currently analyzing a file... // Interrupt with a new direction: $client->beta->sessions->events->send( $session->id, events: [ ['type' => 'user.interrupt'], [ 'type' => 'user.message', 'content' => [ [ 'type' => 'text', 'text' => 'Instead, focus on fixing the bug in line 42.', ], ], ], ], ); ``` ```ruby Ruby # Agent is currently analyzing a file... # Interrupt with a new direction: client.beta.sessions.events.send_( session.id, events: [ {type: "user.interrupt"}, { type: "user.message", content: [ {type: "text", text: "Instead, focus on fixing the bug in line 42."} ] } ] ) ``` The agent acknowledges the interruption and switches to the new task. The interrupted turn ends with a `session.status_idle` event whose `stop_reason` is `end_turn`, the same value as a turn that finishes on its own; there is no stop reason specific to interruption. Stream events from the session to receive real-time updates as the agent works. Only events emitted after the stream is opened are delivered, so open the stream before sending events to avoid a race condition. ```bash cURL # Open the stream first, then send the user message exec {stream}< <( curl --fail-with-body -sS -N \ "https://api.anthropic.com/v1/sessions/$SESSION_ID/events/stream?beta=true" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -H "accept: text/event-stream" ) curl --fail-with-body -sS \ "https://api.anthropic.com/v1/sessions/$SESSION_ID/events?beta=true" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d @- >/dev/null <<'EOF' { "events": [ { "type": "user.message", "content": [{"type": "text", "text": "Summarize the repo README"}] } ] } EOF while IFS= read -r -u "$stream" event_line; do [[ $event_line == data:* ]] || continue event_json=${event_line#data: } case $(jq -r '.type' <<<"$event_json") in agent.message) jq -j '.content[] | select(.type == "text") | .text' <<<"$event_json" ;; session.status_idle) break ;; session.error) printf '\n[Error: %s]\n' "$(jq -r '.error.message // "unknown"' <<<"$event_json")" break ;; esac done exec {stream}<&- ``` ```bash CLI # This workflow does not translate well to a one-off shell command. # Use one of the SDK examples in this code group instead. ``` ```python Python # Open the stream first, then send the user message with client.beta.sessions.events.stream(session.id) as stream: client.beta.sessions.events.send( session.id, events=[ { "type": "user.message", "content": [{"type": "text", "text": "Summarize the repo README"}], }, ], ) for event in stream: match event.type: case "agent.message": for block in event.content: if block.type == "text": print(block.text, end="") case "session.status_idle": break case "session.error": error_message = event.error.message if event.error else "unknown" print(f"\n[Error: {error_message}]") break ``` ```typescript TypeScript // Open the stream first, then send the user message 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: "Summarize the repo README" }] } ] }); for await (const event of stream) { if (event.type === "agent.message") { for (const block of event.content) { if (block.type === "text") { process.stdout.write(block.text); } } } else if (event.type === "session.status_idle") { break; } else if (event.type === "session.error") { console.log(`\n[Error: ${event.error?.message ?? "unknown"}]`); break; } } ``` ```csharp C# // Open the stream first, then send the user message using var stream = await client.Beta.Sessions.Events.WithRawResponse.StreamStreaming(session.ID); await client.Beta.Sessions.Events.Send(session.ID, new() { Events = [ new BetaManagedAgentsUserMessageEventParams { Type = BetaManagedAgentsUserMessageEventParamsType.UserMessage, Content = [ new BetaManagedAgentsTextBlock { Type = BetaManagedAgentsTextBlockType.Text, Text = "Summarize the repo README", }, ], }, ], }); await foreach (var streamEvent in stream.Enumerate()) { if (streamEvent.Value is BetaManagedAgentsAgentMessageEvent message) { foreach (var block in message.Content) { Console.Write(block.Text); } } else if (streamEvent.Value is BetaManagedAgentsSessionStatusIdleEvent) { break; } else if (streamEvent.Value is BetaManagedAgentsSessionErrorEvent error) { Console.WriteLine($"\n[Error: {error.Error?.Message ?? "unknown"}]"); break; } } ``` ```go Go // Open the stream first, then send the user message stream := client.Beta.Sessions.Events.StreamEvents(ctx, session.ID, anthropic.BetaSessionEventStreamParams{}) defer stream.Close() if _, err := client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{ Events: []anthropic.BetaManagedAgentsEventParamsUnion{{ OfUserMessage: &anthropic.BetaManagedAgentsUserMessageEventParams{ Type: anthropic.BetaManagedAgentsUserMessageEventParamsTypeUserMessage, Content: []anthropic.BetaManagedAgentsUserMessageEventParamsContentUnion{{ OfText: &anthropic.BetaManagedAgentsTextBlockParam{ Type: anthropic.BetaManagedAgentsTextBlockTypeText, Text: "Summarize the repo README", }, }}, }, }}, }); err != nil { panic(err) } events: for stream.Next() { switch event := stream.Current().AsAny().(type) { case anthropic.BetaManagedAgentsAgentMessageEvent: // concrete-typed list: BetaManagedAgentsTextBlock for _, block := range event.Content { fmt.Print(block.Text) } case anthropic.BetaManagedAgentsSessionStatusIdleEvent: break events case anthropic.BetaManagedAgentsSessionErrorEvent: fmt.Printf("\n[Error: %s]\n", cmp.Or(event.Error.Message, "unknown")) break events } } if err := stream.Err(); err != nil { panic(err) } ``` ```java Java // Open the stream first, then send the user message 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("Summarize the repo README") .build()) .build() ); Iterable events = stream.stream()::iterator; for (var event : events) { if (event.isAgentMessage()) { event.asAgentMessage().content().forEach(block -> block.text().ifPresent(textBlock -> IO.print(textBlock.text()))); } else if (event.isSessionStatusIdle()) { break; } else if (event.isSessionError()) { // The `message` field spans all error variants; read it from the raw JSON. var errorMessage = event.asSessionError().error()._json().orElse(null) instanceof JsonObject json ? json.values().get("message").asStringOrThrow() : "unknown"; IO.println("\n[Error: " + errorMessage + "]"); break; } } } ``` ```php PHP // Open the stream first, then send the user message $stream = $client->beta->sessions->events->streamStream($session->id); $client->beta->sessions->events->send( $session->id, events: [ [ 'type' => 'user.message', 'content' => [['type' => 'text', 'text' => 'Summarize the repo README']], ], ], ); foreach ($stream as $event) { match ($event->type) { 'agent.message' => array_walk( $event->content, static fn ($block) => $block->type === 'text' ? print($block->text) : null, ), 'session.error' => printf("\n[Error: %s]", $event->error?->message ?? 'unknown'), default => null, }; if ($event->type === 'session.status_idle' || $event->type === 'session.error') { break; } } $stream->close(); ``` ```ruby Ruby # Open the stream first, then send the user message stream = client.beta.sessions.events.stream_events(session.id) client.beta.sessions.events.send_( session.id, events: [{ type: "user.message", content: [{type: "text", text: "Summarize the repo README"}] }] ) stream.each do |event| case event.type in :"agent.message" event.content.each { print it.text } in :"session.status_idle" break in :"session.error" puts "\n[Error: #{event.error&.message || "unknown"}]" break else # ignore other event types end end ``` To reconnect to an existing session without missing events: 1. Open a new stream. 2. List the full event history to seed a set of seen event IDs. 3. Tail the live stream, skipping any events already returned by the history list. ```bash cURL exec {stream}< <( curl --fail-with-body -sS -N \ "https://api.anthropic.com/v1/sessions/$SESSION_ID/events/stream?beta=true" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -H "accept: text/event-stream" ) # Stream is open and buffering. List history before tailing live. declare -A seen_event_ids while IFS= read -r event_id; do seen_event_ids[$event_id]=1 done < <( curl --fail-with-body -sS \ "https://api.anthropic.com/v1/sessions/$SESSION_ID/events?beta=true" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" | jq -r '.data[].id' ) # Tail live events, skipping anything already seen while IFS= read -r -u "$stream" event_line; do [[ $event_line == data:* ]] || continue event_json=${event_line#data: } event_id=$(jq -r '.id' <<<"$event_json") [[ -n ${seen_event_ids[$event_id]+seen} ]] && continue seen_event_ids[$event_id]=1 case $(jq -r '.type' <<<"$event_json") in agent.message) jq -j '.content[] | select(.type == "text") | .text' <<<"$event_json" ;; session.status_idle) break ;; esac done exec {stream}<&- ``` ```bash CLI # This workflow does not translate well to a one-off shell command. # Use one of the SDK examples in this code group instead. ``` ```python Python with client.beta.sessions.events.stream(session.id) as stream: # Stream is open and buffering. List history before tailing live. history = client.beta.sessions.events.list(session.id) seen_event_ids = {past_event.id for past_event in history} # Tail live events, skipping anything already seen for event in stream: if event.type == "event_start" or event.type == "event_delta": # Delta previews aren't enabled on this connection. continue if event.id in seen_event_ids: continue seen_event_ids.add(event.id) match event.type: case "agent.message": for block in event.content: if block.type == "text": print(block.text, end="") case "session.status_idle": break ``` ```typescript TypeScript const seenEventIds = new Set(); const stream = await client.beta.sessions.events.stream(session.id); // Stream is open and buffering. List history before tailing live. for await (const event of client.beta.sessions.events.list(session.id)) { seenEventIds.add(event.id); } // Tail live events, skipping anything already seen for await (const event of stream) { // Preview events (event_start/event_delta) carry no top-level id if (event.type === "event_start" || event.type === "event_delta") continue; if (seenEventIds.has(event.id)) continue; seenEventIds.add(event.id); if (event.type === "agent.message") { for (const block of event.content) { if (block.type === "text") { process.stdout.write(block.text); } } } else if (event.type === "session.status_idle") { break; } } ``` ```csharp C# using var stream = await client.Beta.Sessions.Events.WithRawResponse.StreamStreaming(session.ID); // Stream is open and buffering. List history before tailing live. HashSet seenEventIds = []; var history = await client.Beta.Sessions.Events.List(session.ID); await foreach (var pastEvent in history.Paginate()) { seenEventIds.Add(pastEvent.ID); } // Tail live events, skipping anything already seen await foreach (var streamEvent in stream.Enumerate()) { if (!seenEventIds.Add(streamEvent.ID)) { continue; } if (streamEvent.Value is BetaManagedAgentsAgentMessageEvent message) { foreach (var block in message.Content) { Console.Write(block.Text); } } else if (streamEvent.Value is BetaManagedAgentsSessionStatusIdleEvent) { break; } } ``` ```go Go stream := client.Beta.Sessions.Events.StreamEvents(ctx, session.ID, anthropic.BetaSessionEventStreamParams{}) defer stream.Close() // Stream is open and buffering. List history before tailing live. seenEventIDs := map[string]struct{}{} history := client.Beta.Sessions.Events.ListAutoPaging(ctx, session.ID, anthropic.BetaSessionEventListParams{}) for history.Next() { seenEventIDs[history.Current().ID] = struct{}{} } if err := history.Err(); err != nil { panic(err) } // Tail live events, skipping anything already seen tail: for stream.Next() { event := stream.Current() if _, seen := seenEventIDs[event.ID]; seen { continue } seenEventIDs[event.ID] = struct{}{} switch event := event.AsAny().(type) { case anthropic.BetaManagedAgentsAgentMessageEvent: // concrete-typed list: BetaManagedAgentsTextBlock for _, block := range event.Content { fmt.Print(block.Text) } case anthropic.BetaManagedAgentsSessionStatusIdleEvent: break tail } } if err := stream.Err(); err != nil { panic(err) } ``` ```java Java try (var stream = client.beta().sessions().events().streamStreaming(session.id())) { // Stream is open and buffering. List history before tailing live. // Every event variant carries `id`; read it from the raw JSON to dedup across variants. var seenEventIds = new HashSet(); for (var pastEvent : client.beta().sessions().events().list(session.id()).autoPager()) { if (pastEvent._json().orElseThrow() instanceof JsonObject json) { seenEventIds.add(json.values().get("id").asStringOrThrow()); } } // Tail live events; Set.add returns false for already-seen IDs, skipping the replay. stream.stream() .filter(event -> event._json().orElseThrow() instanceof JsonObject json && seenEventIds.add(json.values().get("id").asStringOrThrow())) .takeWhile(event -> !event.isSessionStatusIdle()) .filter(BetaManagedAgentsStreamSessionEvents::isAgentMessage) .forEach(event -> event.asAgentMessage().content() .forEach(block -> block.text().ifPresent(textBlock -> IO.print(textBlock.text())))); } ``` ```php PHP $stream = $client->beta->sessions->events->streamStream($session->id); // Stream is open and buffering. List history before tailing live. $seenEventIds = []; foreach ($client->beta->sessions->events->list($session->id)->pagingEachItem() as $event) { $seenEventIds[$event->id] = true; } // Tail live events, skipping anything already seen foreach ($stream as $event) { if (isset($seenEventIds[$event->id])) { continue; } $seenEventIds[$event->id] = true; match ($event->type) { 'agent.message' => array_walk( $event->content, static fn ($block) => $block->type === 'text' ? print($block->text) : null, ), default => null, }; if ($event->type === 'session.status_idle') { break; } } $stream->close(); ``` ```ruby Ruby stream = client.beta.sessions.events.stream_events(session.id) # Stream is open and buffering. List history before tailing live. seen_event_ids = Set.new client.beta.sessions.events.list(session.id).auto_paging_each { seen_event_ids << it.id } # Tail live events, skipping anything already seen — Set#add? returns nil for duplicates stream.each do |event| next unless seen_event_ids.add?(event.id) case event.type in :"agent.message" event.content.each { print it.text } in :"session.status_idle" break else # ignore other event types end end ``` Retrieve the full event history for a session: ```bash cURL curl --fail-with-body -sS "https://api.anthropic.com/v1/sessions/$SESSION_ID/events?beta=true" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ | jq -r '.data[] | "[\(.type)] \(.processed_at)"' ``` ```bash CLI ant beta:sessions:events list --session-id "$SESSION_ID" \ --format jsonl --transform '{type,processed_at}' ``` ```python Python events = client.beta.sessions.events.list(session.id) for event in events.data: print(f"[{event.type}] {event.processed_at}") ``` ```typescript TypeScript const events = await client.beta.sessions.events.list(session.id); for (const event of events.data) { console.log(`[${event.type}] ${event.processed_at}`); } ``` ```csharp C# var events = await client.Beta.Sessions.Events.List(session.ID); foreach (var sessionEvent in events.Items) { Console.WriteLine($"[{sessionEvent.Json.GetProperty("type").GetString()}] {sessionEvent.ProcessedAt}"); } ``` ```go Go events, err := client.Beta.Sessions.Events.List(ctx, session.ID, anthropic.BetaSessionEventListParams{}) if err != nil { panic(err) } for _, event := range events.Data { fmt.Printf("[%s] %s\n", event.Type, event.ProcessedAt) } ``` ```java Java var events = client.beta().sessions().events().list(session.id()); for (var event : events.data()) { var eventJson = event._json().orElseThrow().convert(JsonNode.class); var processedAt = eventJson.path("processed_at"); IO.println("[" + eventJson.get("type").asText() + "] " + (processedAt.isTextual() ? processedAt.asText() : "null")); } ``` ```php PHP $events = $client->beta->sessions->events->list($session->id); foreach ($events->data as $event) { $processedAt = ($event->processedAt ?? null)?->format(DATE_RFC3339) ?? 'null'; echo "[{$event->type}] {$processedAt}\n"; } ``` ```ruby Ruby events = client.beta.sessions.events.list(session.id) events.data.each { puts "[#{it.type}] #{it.processed_at}" } ``` Pass a `types` filter to return only specific event types: ```bash cURL curl --fail-with-body -sS "https://api.anthropic.com/v1/sessions/$SESSION_ID/events?beta=true&types[]=agent.tool_use&types[]=agent.tool_result" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ | jq -r '.data[] | "[\(.type)] \(.processed_at)"' ``` ```bash CLI ant beta:sessions:events list --session-id "$SESSION_ID" \ --type agent.tool_use --type agent.tool_result \ --format jsonl --transform '{type,processed_at}' ``` ```python Python events = client.beta.sessions.events.list( session.id, types=["agent.tool_use", "agent.tool_result"], ) for event in events.data: print(f"[{event.type}] {event.processed_at}") ``` ```typescript TypeScript const events = await client.beta.sessions.events.list(session.id, { types: ["agent.tool_use", "agent.tool_result"], }); for (const event of events.data) { console.log(`[${event.type}] ${event.processed_at}`); } ``` ```csharp C# var events = await client.Beta.Sessions.Events.List(session.ID, new() { Types = ["agent.tool_use", "agent.tool_result"], }); foreach (var sessionEvent in events.Items) { Console.WriteLine($"[{sessionEvent.Json.GetProperty("type").GetString()}] {sessionEvent.ProcessedAt}"); } ``` ```go Go events, err := client.Beta.Sessions.Events.List(ctx, session.ID, anthropic.BetaSessionEventListParams{ Types: []string{"agent.tool_use", "agent.tool_result"}, }) if err != nil { panic(err) } for _, event := range events.Data { fmt.Printf("[%s] %s\n", event.Type, event.ProcessedAt) } ``` ```java Java var events = client.beta().sessions().events().list( session.id(), EventListParams.builder() .addType("agent.tool_use") .addType("agent.tool_result") .build()); for (var event : events.data()) { event.agentToolUse().ifPresent(toolUse -> IO.println("[" + toolUse.type() + "] " + toolUse.processedAt())); event.agentToolResult().ifPresent(toolResult -> IO.println("[" + toolResult.type() + "] " + toolResult.processedAt())); } ``` ```php PHP // In PHP, pass the types you want on EventListParams; see the Anthropic PHP SDK. ``` ```ruby Ruby events = client.beta.sessions.events.list( session.id, types: ["agent.tool_use", "agent.tool_result"] ) events.data.each { puts "[#{it.type}] #{it.processed_at}" } ``` ## Event deltas By default, the agent's response text reaches the stream as buffered `agent.message` events, each emitted only after the model request that produced it finishes. Event deltas let you render that text incrementally, as a live preview, while the model is still generating it. A preview is not the response: previews are a best-effort display aid, and the buffered `agent.message` is always the authoritative record. A client that ignores previews still receives a complete, correct stream. ### Opt in to previews Previews are opt-in per stream connection. Add the `event_deltas[]` query parameter to the stream you're reading, repeating it once for each event type you want previewed. Because `[]` is a shell glob pattern, quote the URL whenever you build the request in a shell; the examples percent-encode the brackets as `%5B%5D`, which also works. Both stream endpoints accept the parameter: the session-level stream at `GET /v1/sessions/{session_id}/events/stream`, and each [session thread](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration)'s own stream at `GET /v1/sessions/{session_id}/threads/{thread_id}/stream`. The accepted values are `agent.message` and `agent.thinking`; any other value returns a 400 error, as does a request with more than 100 values. A subagent's previews appear on [that subagent's own thread stream](https://platform.claude.com/docs/en/managed-agents/events-and-streaming#preview-session-thread-events). When a previewed event begins, the stream emits an `event_start` carrying the upcoming event's type and `id`: ```json { "type": "event_start", "event": { "type": "agent.message", "id": "sevt_01abc..." } } ``` For `agent.message`, the start is followed by `event_delta` events carrying incremental text. Each delta names the event it extends in `event_id` and the content block it extends in `delta.index`: ```json { "type": "event_delta", "event_id": "sevt_01abc...", "delta": { "type": "content_delta", "index": 0, "content": { "type": "text", "text": "Here is the summary" } } } ``` When an `agent.thinking` event is previewed, only the `event_start` is emitted. No `event_delta` events follow, and the buffered `agent.thinking` event that concludes the preview carries no thinking content; it is a progress signal, not a content carrier. Unlike persisted events, `event_start` and `event_delta` have no `id` or `processed_at` of their own. The only identifier they carry is the `id` of the event they preview. Event deltas use a different wire format from [Streaming messages](https://platform.claude.com/docs/en/build-with-claude/streaming), and the difference is intentional. A previewed `agent.message` gets a single `event_start` followed only by `event_delta` events. There are no per-content-block start or stop events and no stop event for the previewed event itself. The delta type is `content_delta`, not `content_block_delta`. Accumulator code written for the Messages API does not carry over unchanged. ### Accumulate and reconcile Every SDK that supports event deltas includes an accumulator helper that handles the `index` bookkeeping for you. The Go, Java, Ruby, and C# helpers also key the accumulating preview by the event's `id`; with the Python, TypeScript, and PHP helpers you keep that map yourself and fold each delta into the entry for its `id`. The manual pattern also works in every language when you need custom bookkeeping: apply it to the generated event types. In the manual pattern, treat the preview as a scratch buffer and the buffered event as the record. Key the buffer by `(event_id, index)`. Reconcile per model request: a turn opens with a single `session.status_running` event, then on a turn that completes normally each model request produces, in order, `span.model_request_start`, `event_start`, the `event_delta` events, the buffered `agent.message`, and finally [`span.model_request_end`](https://platform.claude.com/docs/en/managed-agents/reference#event-types) (in the Span events tab). On the wire, this is the previewed portion of that sequence, interleaved with the connection's other buffered events: ```text wrap event_start {"event": {"type": "agent.message", "id": "sevt_01abc..."}} event_delta {"event_id": "sevt_01abc...", "delta": {"type": "content_delta", "index": 0, "content": {"type": "text", "text": "..."}}} ... agent.message {"id": "sevt_01abc...", "content": [...]} ``` The `event_delta` line repeats once per text fragment. Process each event as it arrives: 1. On `event_start`, note the announced `id`. The identifiers always line up: `event_start.event.id`, every `event_delta.event_id`, and the buffered `agent.message`'s `id` are the same value. 2. On each `event_delta`, append `delta.content.text` to the entry at `(event_id, delta.index)` and render the running text. The first delta for an `index` creates that entry. 3. When the buffered `agent.message` arrives, match it by `id`, discard the accumulated preview, and render the message's content instead. 4. On `span.model_request_end`, close any preview that has not been reconciled by its buffered event. No more deltas are coming for it. If the turn errors or is interrupted, the buffered event might never arrive; `span.model_request_end` still does. Guarantees the pattern relies on: * Concatenating a preview's deltas in arrival order, keyed by `(event_id, index)`, gives a prefix of `content[index].text` in the buffered event (a prefix, not necessarily the whole text, because deltas might be shed under load). * A connection emits at most one `event_start` per `event_id`, and the buffered event is the last thing that connection delivers for that `id`. ```bash cURL # Opt in to agent.message previews via event_deltas, then accumulate manually. exec {stream}< <( curl --fail-with-body -sS -N \ "https://api.anthropic.com/v1/sessions/$SESSION_ID/events/stream?beta=true&event_deltas%5B%5D=agent.message" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "accept: text/event-stream" ) curl --fail-with-body -sS \ "https://api.anthropic.com/v1/sessions/$SESSION_ID/events?beta=true" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d @- >/dev/null <<'EOF' { "events": [ { "type": "user.message", "content": [{"type": "text", "text": "In one short sentence, describe what an event delta is."}] } ] } EOF # Accumulate deltas keyed by (message id, content index); the final # agent.message carries the full text, so it replaces every preview for that id. declare -A preview while IFS= read -r -u "$stream" event_line; do [[ $event_line == data:* ]] || continue event_json=${event_line#data: } case $(jq -r '.type' <<<"$event_json") in event_start) preview_id=$(jq -r '.event.id' <<<"$event_json") printf '[event_start id=%s]\n' "$preview_id" ;; event_delta) preview_key=$(jq -r '.event_id + ":" + (.delta.index | tostring)' <<<"$event_json") preview[$preview_key]+=$(jq -r '.delta.content.text' <<<"$event_json") printf '[event_delta] %s\n' "${preview[$preview_key]}" ;; agent.message) msg_id=$(jq -r '.id' <<<"$event_json") for preview_key in "${!preview[@]}"; do [[ $preview_key == "$msg_id":* ]] && unset "preview[$preview_key]" done printf '[agent.message id=%s] ' "$msg_id" jq -j '.content[] | select(.type == "text") | .text' <<<"$event_json" printf '\n' ;; span.model_request_end) for preview_key in "${!preview[@]}"; do printf '[closing unreconciled preview for %s]\n' "${preview_key%%:*}" done preview=() ;; session.status_idle) break ;; esac done exec {stream}<&- ``` ```bash CLI # This workflow does not translate well to a one-off shell command. # Use one of the SDK examples in this code group instead. ``` ```python Python # Preview snapshots, keyed by event id. accumulate_managed_agents_event folds each # event_start / event_delta into an agent.message snapshot; the buffered # agent.message replaces it. previews: dict[str, BetaManagedAgentsAgentMessageEvent] = {} # Opt in to agent.message previews on this connection with client.beta.sessions.events.stream( session.id, event_deltas=["agent.message"] ) as stream: client.beta.sessions.events.send( session.id, events=[ { "type": "user.message", "content": [{"type": "text", "text": "Describe the repo in one sentence."}], }, ], ) for event in stream: match event.type: case "event_start": snapshot = accumulate_managed_agents_event(None, event) if snapshot is not None: previews[event.event.id] = snapshot print(f"event_start {event.event.type} {event.event.id}") case "event_delta": preview = accumulate_managed_agents_event(previews.get(event.event_id), event) if preview is not None: previews[event.event_id] = preview text = "".join(block.text for block in preview.content) print(f"event_delta preview: {text!r}") case "agent.message": # The buffered event is the record: it replaces and closes the preview preview = accumulate_managed_agents_event(previews.pop(event.id, None), event) text = "".join(block.text for block in preview.content) print(f"agent.message {event.id} {text!r}") case "span.model_request_end": # No more deltas are coming. Close any preview whose # buffered event never arrived. for event_id in previews: print(f"span.model_request_end closing preview for {event_id}") previews.clear() case "session.status_idle": break ``` ```typescript TypeScript // Preview snapshots, keyed by event id. `accumulateManagedAgentsEvent` // folds event_start / event_delta previews into an agent.message snapshot. const previews = new Map(); // Opt in to agent.message previews for this connection only const stream = await client.beta.sessions.events.stream(session.id, { event_deltas: ["agent.message"], }); await client.beta.sessions.events.send(session.id, { events: [ { type: "user.message", content: [{ type: "text", text: "Summarize the repo README" }] } ] }); for await (const event of stream) { if (event.type === "event_start") { // 1. Note the announced id and open the snapshot. Deltas and the // buffered event carry the same id. const preview = accumulateManagedAgentsEvent(undefined, event); if (preview) previews.set(event.event.id, preview); console.log(`event_start ${event.event.type} ${event.event.id}`); } else if (event.type === "event_delta") { // 2. Fold the fragment into the snapshot and render it const preview = accumulateManagedAgentsEvent(previews.get(event.event_id), event); if (preview) { previews.set(event.event_id, preview); const text = preview.content.map((block) => block.text).join(""); console.log(`event_delta preview: ${JSON.stringify(text)}`); } } else if (event.type === "agent.message") { // 3. The buffered event is the record: it replaces and closes the preview const message = accumulateManagedAgentsEvent(previews.get(event.id), event); previews.delete(event.id); const text = message.content.map((block) => block.text).join(""); console.log(`agent.message ${event.id} ${JSON.stringify(text)}`); } else if (event.type === "span.model_request_end") { // 4. No more deltas are coming. Close any preview that was never reconciled. for (const eventId of previews.keys()) { console.log(`span.model_request_end closing preview for ${eventId}`); } previews.clear(); } else if (event.type === "session.status_idle") { break; } } stream.controller.abort(); ``` ```csharp C# // Opt in to event deltas: agent.message events are previewed as they are produced. using var stream = await client.Beta.Sessions.Events.WithRawResponse.StreamStreaming( session.ID, new() { EventDeltas = [BetaManagedAgentsDeltaType.AgentMessage] } ); await client.Beta.Sessions.Events.Send(session.ID, new() { Events = [ new BetaManagedAgentsUserMessageEventParams { Type = BetaManagedAgentsUserMessageEventParamsType.UserMessage, Content = [ new BetaManagedAgentsTextBlock { Type = BetaManagedAgentsTextBlockType.Text, Text = "Write a haiku about event streams.", }, ], }, ], }); // Accumulate preview fragments per (event id, content index). The buffered // agent.message that follows carries the complete content, so it replaces the // accumulated preview rather than appending to it. Dictionary> previews = []; await foreach (var streamEvent in stream.Enumerate()) { if (streamEvent.TryPickStartEvent(out var start)) { // A preview opened for the event with this id. This stream only opts in // to agent.message deltas; TryPick* returns false instead of throwing, // so other preview types (including ones added later) are skipped. if (start.Event.TryPickAgentMessage(out var preview)) { Console.WriteLine($"event_start {preview.Type.Raw()} {preview.ID}"); } } else if (streamEvent.TryPickDeltaEvent(out var delta)) { // Insert at a new index, append at an existing one if (!previews.TryGetValue(delta.EventID, out var fragments)) { previews[delta.EventID] = fragments = []; } var index = delta.Delta.Index ?? 0; fragments[index] = fragments.GetValueOrDefault(index, "") + delta.Delta.Content.Text; Console.WriteLine($"event_delta preview: {fragments[index]}"); } else if (streamEvent.TryPickAgentMessageEvent(out var message)) { // Deltas are best-effort: discard the preview and use the buffered event previews.Remove(message.ID); Console.WriteLine($"agent.message {message.ID} {string.Concat(message.Content.Select(block => block.Text))}"); } else if (streamEvent.TryPickSpanModelRequestEndEvent(out _)) { // No more deltas are coming; close any preview that was never reconciled. foreach (var eventId in previews.Keys) { Console.WriteLine($"span.model_request_end closing preview for {eventId}"); } previews.Clear(); } else if (streamEvent.TryPickSessionStatusIdleEvent(out _)) { break; } } ``` ```go Go // Opt in to incremental previews of agent.message events stream := client.Beta.Sessions.Events.StreamEvents(ctx, session.ID, anthropic.BetaSessionEventStreamParams{ EventDeltas: []anthropic.BetaManagedAgentsDeltaType{ anthropic.BetaManagedAgentsDeltaTypeAgentMessage, }, }) if _, err := client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{ Events: []anthropic.BetaManagedAgentsEventParamsUnion{{ OfUserMessage: &anthropic.BetaManagedAgentsUserMessageEventParams{ Type: anthropic.BetaManagedAgentsUserMessageEventParamsTypeUserMessage, Content: []anthropic.BetaManagedAgentsUserMessageEventParamsContentUnion{{ OfText: &anthropic.BetaManagedAgentsTextBlockParam{ Type: anthropic.BetaManagedAgentsTextBlockTypeText, Text: "Write a haiku about the ocean.", }, }}, }, }}, }); err != nil { panic(err) } // The accumulator folds event_start / event_delta fragments into // per-event-id agent.message snapshots. The zero value is ready to use. var previews anthropic.BetaManagedAgentsEventAccumulator deltas: for stream.Next() { event := stream.Current() previews.Accumulate(event) switch event := event.AsAny().(type) { case anthropic.BetaManagedAgentsStartEvent: fmt.Printf("event_start %s %s\n", event.Event.Type, event.Event.ID) case anthropic.BetaManagedAgentsDeltaEvent: fmt.Printf("event_delta preview: %q\n", previews.AgentMessageText(event.EventID)) case anthropic.BetaManagedAgentsAgentMessageEvent: // The buffered event carries the complete content: the accumulator // replaces the preview with it fmt.Printf("agent.message %s %q\n", event.ID, previews.AgentMessageText(event.ID)) case anthropic.BetaManagedAgentsSpanModelRequestEndEvent: // No more deltas are coming for this request. The accumulator // drops its snapshots here, closing any preview that was never // reconciled by a buffered agent.message. fmt.Println("span.model_request_end no more deltas for this request") case anthropic.BetaManagedAgentsSessionStatusIdleEvent: break deltas } } if err := stream.Err(); err != nil { panic(err) } stream.Close() ``` ```java Java // Preview text, keyed by event ID then content index. The buffered agent.message replaces it. Map> previews = new HashMap<>(); // Opt in to agent.message previews on this connection try (var stream = client.beta().sessions().events().streamStreaming( session.id(), EventStreamParams.builder() .addEventDelta(BetaManagedAgentsDeltaType.AGENT_MESSAGE) .build() )) { client.beta().sessions().events().send( session.id(), EventSendParams.builder() .addEvent(BetaManagedAgentsUserMessageEventParams.builder() .type(BetaManagedAgentsUserMessageEventParams.Type.USER_MESSAGE) .addTextContent("Describe the repo in one sentence.") .build()) .build() ); Iterable events = stream.stream()::iterator; for (var event : events) { if (event.isEventStart() && event.asEventStart().event().isAgentMessage()) { var preview = event.asEventStart().event().asAgentMessage(); IO.println("event_start " + preview.type().asString() + " " + preview.id()); } else if (event.isEventDelta()) { var eventDelta = event.asEventDelta(); var fragment = eventDelta.delta(); var buffer = previews .computeIfAbsent(eventDelta.eventId(), _ -> new HashMap<>()) .computeIfAbsent(fragment.index().orElse(0L), _ -> new StringBuilder()); buffer.append(fragment.content().text()); IO.println("event_delta preview: " + buffer); } else if (event.isAgentMessage()) { // The buffered event is the record: drop its preview, render its content var message = event.asAgentMessage(); previews.remove(message.id()); var text = message.content().stream() .flatMap(block -> block.text().stream()) .map(textBlock -> textBlock.text()) .collect(Collectors.joining()); IO.println("agent.message " + message.id() + " " + text); } else if (event.isSpanModelRequestEnd()) { // No more deltas are coming. Close any preview whose buffered event never arrived. previews.keySet().forEach(eventId -> IO.println("span.model_request_end closing preview for " + eventId)); previews.clear(); } else if (event.isSessionStatusIdle()) { break; } } } ``` ```php PHP // In PHP, set eventDeltas on EventStreamParams and accumulate with Anthropic\Lib\Sessions\EventAccumulator. ``` ```ruby Ruby # Opt in to event deltas: agent.message previews stream as incremental fragments. stream = client.beta.sessions.events.stream_events( session.id, event_deltas: [Anthropic::Beta::BetaManagedAgentsDeltaType::AGENT_MESSAGE] ) client.beta.sessions.events.send_( session.id, events: [{ type: "user.message", content: [{type: "text", text: "Give a one-sentence project tagline."}] }] ) # Accumulate preview fragments by (event_id, index) into explicitly mutable # (`+""`) buffers so `<<` can append in place. The buffered agent.message with # the same id is authoritative and replaces whatever the deltas built up. buffers = Hash.new do |by_event, event_id| by_event[event_id] = Hash.new { |fragments, index| fragments[index] = +"" } end stream.each do |event| case event.type in :event_start puts "event_start #{event.event.type} #{event.event.id}" in :event_delta delta = event.delta fragment = delta.content.text buffers[event.event_id][delta.index || 0] << fragment puts "event_delta preview: #{buffers[event.event_id][delta.index || 0].inspect}" in :"agent.message" # Replace: drop the accumulated preview and render the complete event. buffers.delete(event.id) puts "agent.message #{event.id} #{event.content.map(&:text).join.inspect}" in :"span.model_request_end" # No more deltas are coming. Close any preview that was never reconciled. buffers.each_key { |event_id| puts "span.model_request_end closing preview for #{event_id}" } buffers.clear in :"session.status_idle" break else # ignore other event types end end ``` ### Preview session thread events In a [multiagent](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration) session, every session thread has its own event stream at `GET /v1/sessions/{session_id}/threads/{thread_id}/stream`, and it takes the same `event_deltas[]` parameter with the same values. Previews are thread-scoped by design: a connection previews only the thread it's reading. A child thread's previews are delivered on that child's own stream and are never cross-posted to the session-level stream, whose previews stay scoped to the primary thread. To watch a subagent's text as the model generates it, open that subagent's thread stream. The thread stream's path is easy to get wrong: it is `/threads/{thread_id}/stream`, not `/events/stream` (which exists only at the session level), and there is no `/threads/{thread_id}/events/stream` endpoint. The preview events themselves don't change. `event_start` and `event_delta` have the same shape on a thread stream as on the session-level stream, and the [accumulate and reconcile](https://platform.claude.com/docs/en/managed-agents/events-and-streaming#accumulate-and-reconcile) pattern applies as written. The one adjustment is bookkeeping: run one accumulator instance per stream connection. ```bash cURL # List the session's threads and pick a child: child threads carry a non-null # parent_thread_id, and the primary thread's parent_thread_id is null. THREAD_ID=$( curl --fail-with-body -sS \ "https://api.anthropic.com/v1/sessions/$SESSION_ID/threads?beta=true" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" | jq -er 'first(.data[] | select(.parent_thread_id != null)).id' ) # The child thread's stream takes the same event_deltas[] parameter as the # session stream. Percent-encode the brackets (%5B%5D) and quote the URL. exec {stream}< <( curl --fail-with-body -sS -N \ "https://api.anthropic.com/v1/sessions/$SESSION_ID/threads/$THREAD_ID/stream?beta=true&event_deltas%5B%5D=agent.message" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "accept: text/event-stream" ) while IFS= read -r -u "$stream" event_line; do [[ $event_line == data:* ]] || continue event_json=${event_line#data: } case $(jq -r '.type' <<<"$event_json") in event_delta) jq -j '.delta.content.text' <<<"$event_json" ;; agent.message) # The buffered event is the authoritative record; render its content. printf '\n' jq -j '.content[] | select(.type == "text") | .text' <<<"$event_json" printf '\n' ;; session.thread_status_idle) break ;; esac done exec {stream}<&- ``` ```bash CLI # List the session's threads and pick a child: child threads carry a non-null # parent_thread_id, and the primary thread's parent_thread_id is null # (--transform's #(parent_thread_id!=~null) query matches non-null values). THREAD_ID=$(ant beta:sessions:threads list \ --session-id "$SESSION_ID" \ --format raw --transform 'data.#(parent_thread_id!=~null).id' --raw-output) # The child thread's stream takes the same event_deltas parameter as the # session stream, one --event-delta flag per event type to preview. @tostr # re-encodes each text field as a JSON string, so every value stays on one # YAML line and jq's fromjson recovers the original text. transform='{type,frag:delta.content.text|@tostr,text:content.#(type=="text").text|@tostr}' exec {stream}< <(ant beta:sessions:threads:events stream \ --session-id "$SESSION_ID" \ --thread-id "$THREAD_ID" \ --event-delta agent.message \ --transform "$transform" \ --format yaml) type= while IFS= read -r -u "$stream" line; do case "$line" in type:\ session.thread_status_idle) break ;; type:\ *) type=${line#type: } ;; frag:*) [[ $type == event_delta ]] || continue jq -j fromjson <<<"${line#frag: }" ;; text:*) [[ $type == agent.message ]] || continue # The buffered event is the authoritative record; render its content. printf '\n' jq -r fromjson <<<"${line#text: }" ;; esac done exec {stream}<&- ``` ```python Python # List the session's threads and pick a child: child threads carry a non-null # parent_thread_id, and the primary thread's parent_thread_id is null. child_thread = next( thread for thread in client.beta.sessions.threads.list(session.id) if thread.parent_thread_id is not None ) # The child thread's stream takes the same event_deltas parameter as the # session stream. with client.beta.sessions.threads.events.stream( child_thread.id, session_id=session.id, event_deltas=["agent.message"], ) as stream: for event in stream: match event.type: case "event_delta": print(event.delta.content.text, end="") case "agent.message": # The buffered event is the authoritative record; render its content print() for block in event.content: if block.type == "text": print(block.text, end="") print() case "session.thread_status_idle": break ``` ```typescript TypeScript // List the session's threads and pick a child: child threads carry a non-null // parent_thread_id, and the primary thread's parent_thread_id is null. let childThreadId: string | undefined; for await (const thread of client.beta.sessions.threads.list(session.id)) { if (thread.parent_thread_id !== null) { childThreadId = thread.id; break; } } if (!childThreadId) throw new Error("No child thread found"); // The child thread's stream takes the same event_deltas parameter as the // session stream. const stream = await client.beta.sessions.threads.events.stream(childThreadId, { session_id: session.id, event_deltas: ["agent.message"], }); for await (const event of stream) { if (event.type === "event_delta") { process.stdout.write(event.delta.content.text); } else if (event.type === "agent.message") { // The buffered event is the authoritative record; render its content. process.stdout.write("\n"); const text = event.content.map((block) => block.text).join(""); console.log(text); } else if (event.type === "session.thread_status_idle") { break; } } stream.controller.abort(); ``` ```csharp C# // List the session's threads and pick a child: child threads carry a non-null // parent_thread_id, and the primary thread's parent_thread_id is null. var threads = await client.Beta.Sessions.Threads.List(session.ID); var childThread = threads.Items.First(thread => thread.ParentThreadID is not null); // The child thread's stream takes the same event_deltas parameter as the // session stream. using var stream = await client.Beta.Sessions.Threads.Events.WithRawResponse.StreamStreaming( childThread.ID, new() { SessionID = session.ID, EventDeltas = [BetaManagedAgentsDeltaType.AgentMessage] } ); await foreach (var streamEvent in stream.Enumerate()) { if (streamEvent.TryPickDeltaEvent(out var delta)) { Console.Write(delta.Delta.Content.Text); } else if (streamEvent.TryPickAgentMessageEvent(out var message)) { // The buffered event is the authoritative record; render its content. Console.WriteLine(); Console.WriteLine(string.Concat(message.Content.Select(block => block.Text))); } else if (streamEvent.TryPickSessionThreadStatusIdleEvent(out _)) { break; } } ``` ```go Go // List the session's threads and pick a child: child threads carry a non-null // parent_thread_id, and the primary thread's parent_thread_id is null. var childThreadID string threads := client.Beta.Sessions.Threads.ListAutoPaging(ctx, session.ID, anthropic.BetaSessionThreadListParams{}) for threads.Next() { if thread := threads.Current(); thread.ParentThreadID != "" { childThreadID = thread.ID break } } if err := threads.Err(); err != nil { panic(err) } // The child thread's stream takes the same event_deltas parameter as the // session stream; run one read loop per stream connection. stream := client.Beta.Sessions.Threads.Events.StreamEvents(ctx, childThreadID, anthropic.BetaSessionThreadEventStreamParams{ SessionID: session.ID, EventDeltas: []anthropic.BetaManagedAgentsDeltaType{ anthropic.BetaManagedAgentsDeltaTypeAgentMessage, }, }) threadDeltas: for stream.Next() { switch event := stream.Current().AsAny().(type) { case anthropic.BetaManagedAgentsDeltaEvent: fmt.Print(event.Delta.Content.Text) case anthropic.BetaManagedAgentsAgentMessageEvent: // The buffered event is the authoritative record; render its content. fmt.Println() // concrete-typed list: BetaManagedAgentsTextBlock for _, block := range event.Content { fmt.Print(block.Text) } fmt.Println() case anthropic.BetaManagedAgentsSessionThreadStatusIdleEvent: break threadDeltas } } if err := stream.Err(); err != nil { panic(err) } stream.Close() ``` ```java Java // List the session's threads and pick a child: child threads carry a non-null // parent_thread_id, and the primary thread's parent_thread_id is null. var childThread = client.beta().sessions().threads().list(session.id()).autoPager().stream() .filter(thread -> thread.parentThreadId().isPresent()) .findFirst() .orElseThrow(); // The child thread's stream takes the same event_deltas parameter as the session // stream. Its params class shares the session-level one's simple name, so qualify it. try (var stream = client.beta().sessions().threads().events().streamStreaming( childThread.id(), com.anthropic.models.beta.sessions.threads.events.EventStreamParams.builder() .sessionId(session.id()) .addEventDelta(BetaManagedAgentsDeltaType.AGENT_MESSAGE) .build() )) { Iterable events = stream.stream()::iterator; for (var event : events) { if (event.isEventDelta()) { IO.print(event.asEventDelta().delta().content().text()); } else if (event.isAgentMessage()) { // The buffered event is the authoritative record; render its content. IO.println(); event.asAgentMessage().content().forEach(block -> block.text().ifPresent(textBlock -> IO.print(textBlock.text()))); IO.println(); } else if (event.isSessionThreadStatusIdle()) { break; } } } ``` ```php PHP // In PHP, set eventDeltas on the thread EventStreamParams and accumulate with Anthropic\Lib\Sessions\EventAccumulator. ``` ```ruby Ruby # List the session's threads and pick a child: child threads carry a non-null # parent_thread_id, and the primary thread's parent_thread_id is null. child_thread = client.beta.sessions.threads.list(session.id).to_enum.find { it.parent_thread_id } # The child thread's stream takes the same event_deltas parameter as the # session stream. stream = client.beta.sessions.threads.events.stream_events( child_thread.id, session_id: session.id, event_deltas: [Anthropic::Beta::BetaManagedAgentsDeltaType::AGENT_MESSAGE] ) stream.each do |event| case event.type in :event_delta print event.delta.content.text in :"agent.message" # The buffered event is the authoritative record; render its content. puts event.content.each { print it.text } puts in :"session.thread_status_idle" break else # ignore other event types end end ``` The read loop exits on [`session.thread_status_idle`](https://platform.claude.com/docs/en/managed-agents/reference#event-types), the event emitted when the session thread's turn finishes and the thread goes idle. ### Limitations Previews are tuned for responsiveness. Build against these constraints: * **Best effort:** Under load, the server might shed deltas for an event. When it does, you receive a contiguous prefix of the text and then no further deltas for that event. The buffered `agent.message` still arrives complete. Never treat an accumulated preview as final. * **No replay on reconnect:** Deltas are delivered only to the connection that opted in, while it is open. This applies to the session-level stream and to each session thread stream alike, and a connection opened after a model request started receives no deltas for that in-flight event. If the stream drops, follow the [reconnect procedure](https://platform.claude.com/docs/en/managed-agents/events-and-streaming#integrating-events) in the Streaming events tab: reopen the stream and list the event history. The history includes any buffered events emitted while you were disconnected, including the `agent.message` your preview was waiting for. There is no way to re-request missed deltas. * **One thread, text only:** Previews cover assistant text on the thread the connection is reading. Tool use, tool results, MCP results, and activity on any other [session thread](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration) are never previewed on that connection. * **Start-only `agent.thinking`:** An `agent.thinking` preview emits only the `event_start` as a signal that a thinking block has started; no `event_delta` events follow it. * **Never persisted:** `event_start` and `event_delta` exist only on the live stream. They do not appear in the session's event history (`GET /v1/sessions/{session_id}/events`) or in any session thread's event history. ### Troubleshoot previews If the stream doesn't behave as you expect: | You see | What it means | | ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | A stream with buffered events but no `event_start` or `event_delta` | The connection you're reading didn't opt in (`event_deltas[]` applies per connection, not per session), or the turn never touched the thread you're streaming. Previews are thread-scoped, so list the session's threads (`GET /v1/sessions/{session_id}/threads`) to find which one ran. | | A 404 on the stream URL | The path or an ID is wrong, or the request carries no managed-agents beta header at all. The thread endpoints are beta-gated, so without the header they don't exist. | | A 400 naming `event_deltas` | Only `agent.message` and `agent.thinking` are accepted. | ## Additional scenarios ### Handling custom tool calls When the agent invokes a [custom tool](https://platform.claude.com/docs/en/managed-agents/tools#custom-tools): 1. The session emits an `agent.custom_tool_use` event containing the tool name and input. 2. The session pauses with a `session.status_idle` event containing `stop_reason: requires_action`. The blocking event IDs are in the `stop_reason.event_ids` array. 3. Execute the tool in your system and send a `user.custom_tool_result` event for each, passing the event ID in the `custom_tool_use_id` parameter along with the result content. 4. Once all blocking events are resolved, the session transitions back to `running`. ```bash cURL exec {stream_fd}< <(curl --fail-with-body -sS -N \ "https://api.anthropic.com/v1/sessions/$SESSION_ID/events/stream?beta=true" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -H "accept: text/event-stream") while IFS= read -r -u "$stream_fd" line; do [[ $line == data:* ]] || continue event_json="${line#data: }" stop_reason=$(jq -r 'select(.type == "session.status_idle") | .stop_reason.type // empty' <<<"$event_json") case "$stop_reason" in requires_action) while IFS= read -r event_id; do # Execute the tool and send the result back result=$(call_tool "$event_id") jq -n --arg id "$event_id" --arg result "$result" \ '{events: [{type: "user.custom_tool_result", custom_tool_use_id: $id, content: [{type: "text", text: $result}]}]}' | curl --fail-with-body -sS \ "https://api.anthropic.com/v1/sessions/$SESSION_ID/events?beta=true" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d @- done < <(jq -r '.stop_reason.event_ids[]' <<<"$event_json") ;; end_turn) break ;; esac done exec {stream_fd}<&- ``` ```bash CLI # This workflow does not translate well to a one-off shell command. # Use one of the SDK examples in this code group instead. ``` ```python Python with client.beta.sessions.events.stream(session.id) as stream: for event in stream: if event.type == "session.status_idle" and (stop_reason := event.stop_reason): match stop_reason.type: case "requires_action": for event_id in stop_reason.event_ids: # Look up the custom tool use event and execute it tool_event = events_by_id[event_id] result = call_tool(tool_event.name, tool_event.input) # Send the result back client.beta.sessions.events.send( session.id, events=[ { "type": "user.custom_tool_result", "custom_tool_use_id": event_id, "content": [{"type": "text", "text": result}], }, ], ) case "end_turn": break ``` ```typescript TypeScript const stream = await client.beta.sessions.events.stream(session.id); for await (const event of stream) { if (event.type !== "session.status_idle") continue; if (event.stop_reason.type === "end_turn") break; if (event.stop_reason.type !== "requires_action") continue; for (const eventId of event.stop_reason.event_ids) { // Look up the custom tool use event and execute it const toolEvent = eventsById.get(eventId); if (!toolEvent) continue; const result = await callTool(toolEvent.name, toolEvent.input); // Send the result back await client.beta.sessions.events.send(session.id, { events: [ { type: "user.custom_tool_result", custom_tool_use_id: eventId, content: [{ type: "text", text: result }], }, ], }); } } ``` ```csharp C# await foreach (var streamEvent in client.Beta.Sessions.Events.StreamStreaming(session.ID)) { if (streamEvent.Value is not BetaManagedAgentsSessionStatusIdleEvent idle) continue; if (idle.StopReason?.Value is BetaManagedAgentsSessionRequiresAction requiresAction) { foreach (var eventId in requiresAction.EventIds) { // Look up the custom tool use event and execute it var toolEvent = eventsById[eventId]; var result = await CallTool(toolEvent.Name, toolEvent.Input); // Send the result back await client.Beta.Sessions.Events.Send(session.ID, new() { Events = [ new BetaManagedAgentsUserCustomToolResultEventParams { Type = BetaManagedAgentsUserCustomToolResultEventParamsType.UserCustomToolResult, CustomToolUseID = eventId, Content = [ new BetaManagedAgentsTextBlock { Type = BetaManagedAgentsTextBlockType.Text, Text = result, }, ], }, ], }); } } else if (idle.StopReason?.Value is BetaManagedAgentsSessionEndTurn) { break; } } ``` ```go Go stream := client.Beta.Sessions.Events.StreamEvents(ctx, session.ID, anthropic.BetaSessionEventStreamParams{}) defer stream.Close() loop: for stream.Next() { event, ok := stream.Current().AsAny().(anthropic.BetaManagedAgentsSessionStatusIdleEvent) if !ok { continue } switch stopReason := event.StopReason.AsAny().(type) { case anthropic.BetaManagedAgentsSessionRequiresAction: for _, eventID := range stopReason.EventIDs { // Look up the custom tool use event and execute it toolEvent := eventsByID[eventID] result := callTool(toolEvent.Name, toolEvent.Input) // Send the result back if _, err := client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{ Events: []anthropic.BetaManagedAgentsEventParamsUnion{{ OfUserCustomToolResult: &anthropic.BetaManagedAgentsUserCustomToolResultEventParams{ Type: anthropic.BetaManagedAgentsUserCustomToolResultEventParamsTypeUserCustomToolResult, CustomToolUseID: eventID, Content: []anthropic.BetaManagedAgentsUserCustomToolResultEventParamsContentUnion{{ OfText: &anthropic.BetaManagedAgentsTextBlockParam{ Type: anthropic.BetaManagedAgentsTextBlockTypeText, Text: result, }, }}, }, }}, }); err != nil { panic(err) } } case anthropic.BetaManagedAgentsSessionEndTurn: break loop } } if err := stream.Err(); err != nil { panic(err) } ``` ```java Java try (var stream = client.beta().sessions().events().streamStreaming(session.id())) { stream.stream() .filter(BetaManagedAgentsStreamSessionEvents::isSessionStatusIdle) .map(idleEvent -> idleEvent.asSessionStatusIdle().stopReason()) .takeWhile(stopReason -> !stopReason.isEndTurn()) .filter(stopReason -> stopReason.isRequiresAction()) .flatMap(stopReason -> stopReason.asRequiresAction().eventIds().stream()) .forEach(eventId -> { // Look up the custom tool use event and execute it var toolEvent = eventsById.get(eventId); var result = callTool(toolEvent.name(), toolEvent.input()); // Send the result back client.beta().sessions().events().send( session.id(), EventSendParams.builder() .addEvent(BetaManagedAgentsUserCustomToolResultEventParams.builder() .type(BetaManagedAgentsUserCustomToolResultEventParams.Type.USER_CUSTOM_TOOL_RESULT) .customToolUseId(eventId) .addTextContent(result) .build()) .build()); }); } ``` ```php PHP $stream = $client->beta->sessions->events->streamStream($session->id); foreach ($stream as $event) { if ($event->type === 'session.status_idle' && $event->stopReason) { if ($event->stopReason->type === 'requires_action') { foreach ($event->stopReason->eventIDs as $eventId) { // Look up the custom tool use event and execute it $toolEvent = $eventsById[$eventId]; $result = callTool($toolEvent->name, $toolEvent->input); // Send the result back $client->beta->sessions->events->send( $session->id, events: [ [ 'type' => 'user.custom_tool_result', 'custom_tool_use_id' => $eventId, 'content' => [['type' => 'text', 'text' => $result]], ], ], ); } } elseif ($event->stopReason->type === 'end_turn') { break; } } } ``` ```ruby Ruby client.beta.sessions.events.stream_events(session.id).each do |event| case event in {type: :"session.status_idle", stop_reason: {type: :requires_action, event_ids:}} event_ids.each do |event_id| # Look up the custom tool use event and execute it tool_event = events_by_id[event_id] result = call_tool.call(tool_event.name, tool_event.input) # Send the result back client.beta.sessions.events.send_( session.id, events: [ { type: "user.custom_tool_result", custom_tool_use_id: event_id, content: [{type: "text", text: result}] } ] ) end in {type: :"session.status_idle", stop_reason: {type: :end_turn}} break else end end ``` ### Tool confirmation When a [permission policy](https://platform.claude.com/docs/en/managed-agents/permission-policies) requires confirmation before a tool executes: 1. The session emits an `agent.tool_use` or `agent.mcp_tool_use` event. 2. The session pauses with a `session.status_idle` event containing `stop_reason: requires_action`. The blocking event IDs are in the `stop_reason.event_ids` array. 3. Send a `user.tool_confirmation` event for each, passing the event ID in the `tool_use_id` parameter. Set `result` to `"allow"` or `"deny"`. Use `deny_message` to explain a denial. 4. Once all blocking events are resolved, the session transitions back to `running`. ```bash cURL exec {stream_fd}< <(curl --fail-with-body -sS -N \ "https://api.anthropic.com/v1/sessions/$SESSION_ID/events/stream?beta=true" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -H "accept: text/event-stream") while IFS= read -r -u "$stream_fd" line; do [[ $line == data:* ]] || continue event_json="${line#data: }" stop_reason=$(jq -r 'select(.type == "session.status_idle") | .stop_reason.type // empty' <<<"$event_json") case "$stop_reason" in requires_action) while IFS= read -r event_id; do # Approve the pending tool call jq -n --arg id "$event_id" \ '{events: [{type: "user.tool_confirmation", tool_use_id: $id, result: "allow"}]}' | curl --fail-with-body -sS \ "https://api.anthropic.com/v1/sessions/$SESSION_ID/events?beta=true" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d @- done < <(jq -r '.stop_reason.event_ids[]' <<<"$event_json") ;; end_turn) break ;; esac done exec {stream_fd}<&- ``` ```bash CLI # This workflow does not translate well to a one-off shell command. # Use one of the SDK examples in this code group instead. ``` ```python Python with client.beta.sessions.events.stream(session.id) as stream: for event in stream: if event.type == "session.status_idle" and (stop_reason := event.stop_reason): match stop_reason.type: case "requires_action": for event_id in stop_reason.event_ids: # Approve the pending tool call client.beta.sessions.events.send( session.id, events=[ { "type": "user.tool_confirmation", "tool_use_id": event_id, "result": "allow", }, ], ) case "end_turn": break ``` ```typescript TypeScript const stream = await client.beta.sessions.events.stream(session.id); for await (const event of stream) { if (event.type !== "session.status_idle") continue; if (event.stop_reason.type === "end_turn") break; if (event.stop_reason.type !== "requires_action") continue; for (const eventId of event.stop_reason.event_ids) { // Approve the pending tool call await client.beta.sessions.events.send(session.id, { events: [ { type: "user.tool_confirmation", tool_use_id: eventId, result: "allow", }, ], }); } } ``` ```csharp C# await foreach (var streamEvent in client.Beta.Sessions.Events.StreamStreaming(session.ID)) { if (streamEvent.Value is not BetaManagedAgentsSessionStatusIdleEvent idle) continue; if (idle.StopReason?.Value is BetaManagedAgentsSessionRequiresAction requiresAction) { foreach (var eventId in requiresAction.EventIds) { // Approve the pending tool call await client.Beta.Sessions.Events.Send(session.ID, new() { Events = [ new BetaManagedAgentsUserToolConfirmationEventParams { Type = BetaManagedAgentsUserToolConfirmationEventParamsType.UserToolConfirmation, ToolUseID = eventId, Result = BetaManagedAgentsUserToolConfirmationEventParamsResult.Allow, }, ], }); } } else if (idle.StopReason?.Value is BetaManagedAgentsSessionEndTurn) { break; } } ``` ```go Go stream := client.Beta.Sessions.Events.StreamEvents(ctx, session.ID, anthropic.BetaSessionEventStreamParams{}) defer stream.Close() loop: for stream.Next() { event, ok := stream.Current().AsAny().(anthropic.BetaManagedAgentsSessionStatusIdleEvent) if !ok { continue } switch stopReason := event.StopReason.AsAny().(type) { case anthropic.BetaManagedAgentsSessionRequiresAction: for _, eventID := range stopReason.EventIDs { // Approve the pending tool call if _, err := client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{ Events: []anthropic.BetaManagedAgentsEventParamsUnion{{ OfUserToolConfirmation: &anthropic.BetaManagedAgentsUserToolConfirmationEventParams{ Type: anthropic.BetaManagedAgentsUserToolConfirmationEventParamsTypeUserToolConfirmation, ToolUseID: eventID, Result: anthropic.BetaManagedAgentsUserToolConfirmationEventParamsResultAllow, }, }}, }); err != nil { panic(err) } } case anthropic.BetaManagedAgentsSessionEndTurn: break loop } } if err := stream.Err(); err != nil { panic(err) } ``` ```java Java try (var stream = client.beta().sessions().events().streamStreaming(session.id())) { stream.stream() .filter(BetaManagedAgentsStreamSessionEvents::isSessionStatusIdle) .map(idleEvent -> idleEvent.asSessionStatusIdle().stopReason()) .takeWhile(stopReason -> !stopReason.isEndTurn()) .filter(stopReason -> stopReason.isRequiresAction()) .flatMap(stopReason -> stopReason.asRequiresAction().eventIds().stream()) // Approve each pending tool call .forEach(toolUseId -> client.beta().sessions().events().send( session.id(), EventSendParams.builder() .addEvent(BetaManagedAgentsUserToolConfirmationEventParams.builder() .type(BetaManagedAgentsUserToolConfirmationEventParams.Type.USER_TOOL_CONFIRMATION) .toolUseId(toolUseId) .result(BetaManagedAgentsUserToolConfirmationEventParams.Result.ALLOW) .build()) .build())); } ``` ```php PHP $stream = $client->beta->sessions->events->streamStream($session->id); foreach ($stream as $event) { if ($event->type === 'session.status_idle' && $event->stopReason) { if ($event->stopReason->type === 'requires_action') { foreach ($event->stopReason->eventIDs as $eventId) { // Approve the pending tool call $client->beta->sessions->events->send( $session->id, events: [ [ 'type' => 'user.tool_confirmation', 'tool_use_id' => $eventId, 'result' => 'allow', ], ], ); } } elseif ($event->stopReason->type === 'end_turn') { break; } } } ``` ```ruby Ruby client.beta.sessions.events.stream_events(session.id).each do |event| case event in {type: :"session.status_idle", stop_reason: {type: :requires_action, event_ids:}} event_ids.each do |event_id| # Approve the pending tool call client.beta.sessions.events.send_( session.id, events: [ {type: "user.tool_confirmation", tool_use_id: event_id, result: "allow"} ] ) end in {type: :"session.status_idle", stop_reason: {type: :end_turn}} break else end end ``` ### Resuming an idle session Sessions persist between interactions. Conversation history is preserved unless the session is explicitly deleted. When a session goes idle, its sandbox is checkpointed, preserving the full sandbox state, including the filesystem, installed packages, and any files the agent created. This allows you to resume cleanly from inactivity. While session history is persisted until deleted, sandbox state is only preserved for 30 days after the sandbox is created. Activity does not extend this window: after 30 days the sandbox state (files, installed tools, and so on) is unrecoverable, and a resumed session starts from a fresh sandbox. If your workflow depends on sandbox contents, have the agent write important artifacts to [outputs](https://platform.claude.com/docs/en/managed-agents/define-outcomes#retrieving-deliverables) before the window ends. To resume a session, send a `user.message` event to it as usual: ```bash cURL # In production, pass the stored ID of the session you want to resume. curl --fail-with-body -sS "https://api.anthropic.com/v1/sessions/$SESSION_ID/events?beta=true" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d @- <<'EOF' { "events": [ { "type": "user.message", "content": [ {"type": "text", "text": "Now run the tests against the changes you made earlier."} ] } ] } EOF ``` ```bash CLI # In production, pass the stored ID of the session you want to resume. ant beta:sessions:events send --session-id "$SESSION_ID" <<'YAML' events: - type: user.message content: - type: text text: Now run the tests against the changes you made earlier. YAML ``` ```python Python # Resume a previously created session by sending it a new user.message event. # In production, pass the stored ID of the session you want to resume. client.beta.sessions.events.send( session.id, events=[ { "type": "user.message", "content": [ { "type": "text", "text": "Now run the tests against the changes you made earlier.", }, ], }, ], ) ``` ```typescript TypeScript // Resume a previously created session by sending it a new user event. // In production, pass the stored ID of the session you want to resume. await client.beta.sessions.events.send(session.id, { events: [ { type: "user.message", content: [ { type: "text", text: "Now run the tests against the changes you made earlier.", }, ], }, ], }); ``` ```csharp C# // Resume a previously created session by ID. In production, pass the // session ID you stored when the session was created. await client.Beta.Sessions.Events.Send(session.ID, new() { Events = [ new BetaManagedAgentsUserMessageEventParams { Type = BetaManagedAgentsUserMessageEventParamsType.UserMessage, Content = [ new BetaManagedAgentsTextBlock { Type = BetaManagedAgentsTextBlockType.Text, Text = "Now run the tests against the changes you made earlier.", }, ], }, ], }); ``` ```go Go // Resume a previously created session by sending it a new user.message // event. In production, pass the stored ID of the session to resume. if _, err := client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{ Events: []anthropic.BetaManagedAgentsEventParamsUnion{{ OfUserMessage: &anthropic.BetaManagedAgentsUserMessageEventParams{ Type: anthropic.BetaManagedAgentsUserMessageEventParamsTypeUserMessage, Content: []anthropic.BetaManagedAgentsUserMessageEventParamsContentUnion{{ OfText: &anthropic.BetaManagedAgentsTextBlockParam{ Type: anthropic.BetaManagedAgentsTextBlockTypeText, Text: "Now run the tests against the changes you made earlier.", }, }}, }, }}, }); err != nil { panic(err) } ``` ```java Java // Resume a previously created session by ID. In production, pass the // session ID you stored when the session was created. client.beta().sessions().events().send( session.id(), EventSendParams.builder() .addEvent(BetaManagedAgentsUserMessageEventParams.builder() .type(BetaManagedAgentsUserMessageEventParams.Type.USER_MESSAGE) .addTextContent("Now run the tests against the changes you made earlier.") .build()) .build()); ``` ```php PHP // Resume a previously created session by sending it a new user.message event. // In production, pass the session ID you stored when the session was created. $client->beta->sessions->events->send( $session->id, events: [ [ 'type' => 'user.message', 'content' => [ [ 'type' => 'text', 'text' => 'Now run the tests against the changes you made earlier.', ], ], ], ], ); ``` ```ruby Ruby # Resuming a session is just sending the next event to it. In production, # pass the session ID you stored when the session was created. client.beta.sessions.events.send_( session.id, events: [ { type: "user.message", content: [ {type: "text", text: "Now run the tests against the changes you made earlier."} ] } ] ) ``` ### Reaching a session budget A session created with a [budget](https://platform.claude.com/docs/en/managed-agents/budgets) pauses instead of overspending. When the session's tracked list cost reaches the cap, the platform pauses each thread before its next model request, and the session goes idle with a `stop_reason` of `budget_reached` rather than terminating. The request that carried the total past the cap runs to completion, so the `list_cost` reported by the `session.usage` snapshot can read [at or a fraction past the cap](https://platform.claude.com/docs/en/managed-agents/budgets#when-a-session-reaches-its-budget). On the stream, the pause arrives as three events, in order: 1. `session.thread_status_idle` with `stop_reason: budget_reached`, for each thread as it pauses. 2. `session.usage`, a snapshot of the session's cumulative usage and tracked list cost. 3. `session.status_idle` with `stop_reason: budget_reached`. The `session.usage` event always immediately precedes this idle. A thread whose final request both crosses the cap and completes its turn reports `end_turn` on its own `session.thread_status_idle` event while the session still reports `budget_reached`; key on the session-level `stop_reason` to detect the pause. While the session is at its cap, it accepts only the events that settle work already in flight: `user.tool_confirmation`, `user.tool_result`, `user.custom_tool_result`, and `user.interrupt`. Any event that would start new work, including `user.message`, is rejected with a 400 error naming that list. When a session has both a thread waiting on a tool ask and a thread paused at the cap, the session-level `stop_reason` is `requires_action`, not `budget_reached`: settling the ask doesn't trigger a model request, so respond to it as usual. No event resumes a session paused at its cap. Instead, update the session's budget: changing the cap to any value above the consumed list cost, or removing the budget by updating the session with `"budget": null`, resumes the paused work automatically. See [Session budgets](https://platform.claude.com/docs/en/managed-agents/budgets) for how list cost is tracked and the full budget update semantics. ### Sending system messages `system.message` is currently supported by Claude Opus 4.8, Claude Fable 5, Claude Mythos 5, and Claude Opus 5. If the agent's primary model does not support mid-conversation system injection, the event is rejected with a `model_does_not_support_mid_conversation_system` validation error; subagent models are not checked, because `system.message` lands on the primary thread only. Send a `system.message` event to give the agent privileged system-level context that applies to the accompanying turn and all subsequent turns. Unlike the `system` field on the agent definition (which sets the top-level system prompt), `system.message` content is appended to the session's system context as a `role: "system"` turn rather than replacing that prompt. Use it when the agent needs updated system-level guidance mid-session: a different persona, revised constraints, or context fetched at runtime that should shape the model's behavior going forward. ```bash cURL curl --fail-with-body -sS "https://api.anthropic.com/v1/sessions/$SESSION_ID/events?beta=true" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d @- <<'EOF' { "events": [ { "type": "system.message", "content": [ {"type": "text", "text": "The user's current timezone is America/New_York."} ] } ] } EOF ``` ```bash CLI ant beta:sessions:events send --session-id "$SESSION_ID" <<'YAML' events: - type: system.message content: - type: text text: "The user's current timezone is America/New_York." YAML ``` ```python Python client.beta.sessions.events.send( session.id, events=[ { "type": "system.message", "content": [ { "type": "text", "text": "The user's current timezone is America/New_York.", }, ], }, ], ) ``` ```typescript TypeScript await client.beta.sessions.events.send(session.id, { events: [ { type: "system.message", content: [ { type: "text", text: "The user's current timezone is America/New_York.", }, ], }, ], }); ``` ```csharp C# await client.Beta.Sessions.Events.Send(session.ID, new() { Events = [ new BetaManagedAgentsSystemMessageEventParams { Type = BetaManagedAgentsSystemMessageEventParamsType.SystemMessage, Content = [ new BetaManagedAgentsSystemContentBlock { Type = BetaManagedAgentsSystemContentBlockType.Text, Text = "The user's current timezone is America/New_York.", }, ], }, ], }); ``` ```go Go if _, err := client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{ Events: []anthropic.BetaManagedAgentsEventParamsUnion{{ OfSystemMessage: &anthropic.BetaManagedAgentsSystemMessageEventParams{ Type: anthropic.BetaManagedAgentsSystemMessageEventParamsTypeSystemMessage, Content: []anthropic.BetaManagedAgentsSystemContentBlockParam{{ Type: anthropic.BetaManagedAgentsSystemContentBlockTypeText, Text: "The user's current timezone is America/New_York.", }}, }, }}, }); err != nil { panic(err) } ``` ```java Java client.beta().sessions().events().send( session.id(), EventSendParams.builder() .addEvent(BetaManagedAgentsSystemMessageEventParams.builder() .type(BetaManagedAgentsSystemMessageEventParams.Type.SYSTEM_MESSAGE) .addTextContent("The user's current timezone is America/New_York.") .build()) .build()); ``` ```php PHP $client->beta->sessions->events->send( $session->id, events: [ [ 'type' => 'system.message', 'content' => [ [ 'type' => 'text', 'text' => "The user's current timezone is America/New_York.", ], ], ], ], ); ``` ```ruby Ruby client.beta.sessions.events.send_( session.id, events: [ { type: "system.message", content: [ {type: "text", text: "The user's current timezone is America/New_York."} ] } ] ) ``` While the session is idle with `stop_reason: requires_action`, a `system.message` is accepted only when it trails a tool result event in the same request; sent on its own or with a `user.message`, it is rejected until the pending tool events are resolved. `content` accepts 1–1000 text items. ### Tracking usage The session object includes a `usage` field with the session's cumulative usage: token counts, server tool use, active time, and the tracked list cost. Fetch the session after it goes idle to read the latest totals. ```json { "id": "sesn_01...", "status": "idle", "usage": { "input_tokens": 5000, "output_tokens": 3200, "cache_read_input_tokens": 20000, "cache_creation": { "ephemeral_5m_input_tokens": 2000, "ephemeral_1h_input_tokens": 0 }, "list_cost": { "amount": "187", "currency": "USD" }, "active_seconds": 342.5, "server_tool_use": { "web_search_requests": 3, "web_fetch_requests": 0 } } } ``` `input_tokens` reports uncached input tokens and `output_tokens` reports total output tokens across all model calls in the session. The `cache_read_input_tokens` field reports tokens read from the prompt cache, and the `cache_creation` object breaks down cache-creation tokens by cache lifetime (`ephemeral_5m_input_tokens` and `ephemeral_1h_input_tokens`). Cache entries use a 5-minute TTL by default, so back-to-back turns within that window benefit from cache reads, which reduce per-token cost. `list_cost` is the session's cumulative consumption priced at public list rates, as a whole number of cents in a string, with a currency code. `active_seconds` is the cumulative time during which the session had at least one thread running; overlapping activity from concurrent threads is counted once, unlike the `active_seconds` in the session's `stats` object, which sums each thread's own active time. This deduplicated figure is the duration the session's runtime cost is priced on. `server_tool_use` counts server-executed tool requests for pricing: web search requests are priced into list cost per request, and web fetch requests carry no per-request charge and aren't metered, so `web_fetch_requests` reads `0`. Each [session thread](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration)'s own `usage` carries `list_cost` and `active_seconds` too. Per-thread figures are rounded independently and exclude the session's running-time cost, so they don't sum exactly to the session's `list_cost`; the session figure is the authoritative one. You don't have to poll the session to observe these totals. The `session.usage` event carries the same cumulative snapshot (the `usage` object, plus the session's `budget`, which is `null` when the session has none) on the session stream and in the event history. It is emitted on idle transitions rather than on a timer: the session emits one immediately before it goes idle, whatever the stop reason, and one when a thread pauses at a [session budget](https://platform.claude.com/docs/en/managed-agents/budgets). A stream reader therefore sees the final cost of a turn, or of the work that hit a budget, without an extra fetch. To enforce a spend limit, set a [session budget](https://platform.claude.com/docs/en/managed-agents/budgets) rather than polling usage and stopping the session yourself. The platform prices the session's consumption continuously and pauses each thread before its next model request once the session's list cost reaches the cap; see [Reaching a session budget](https://platform.claude.com/docs/en/managed-agents/events-and-streaming#reaching-a-session-budget) for what that looks like on the stream. ## Console observability The Claude Console provides a visual timeline view of your agent sessions. Navigate to the Claude Managed Agents section in the Console to see: * **Session list:** All sessions with their status, creation time, and agent * **Tracing view:** A chronological view of events (content, timestamps, token usage) within a session. Tracing views are only accessible to Developers and Admins. * **Tool execution:** Details of each tool call and its result ## Debugging tips * **Check session events:** Session errors are conveyed through the `session.error` event * **Review tool results:** Tool execution failures often explain unexpected agent behavior * **Track token usage:** Monitor token consumption to optimize prompts and reduce costs * **Use system prompts:** Add logging instructions to the system prompt to make the agent explain its reasoning * **Troubleshoot previews:** If a stream that opts in to event deltas doesn't behave as you expect, see [Troubleshoot previews](https://platform.claude.com/docs/en/managed-agents/events-and-streaming#troubleshoot-previews) --- title: Session operations url: https://platform.claude.com/docs/en/managed-agents/session-operations description: Retrieve, list, update, archive, and delete Claude Managed Agents sessions. --- Once a session exists, use these operations to read, update, archive, or delete it. See [Start a session](https://platform.claude.com/docs/en/managed-agents/sessions) for creating a session and sending it work. Managed Agents API requests require the `managed-agents-2026-04-01` beta header, except memory store endpoints, which use `agent-memory-2026-07-22` instead. The SDK sets the correct beta header automatically. See [Beta headers](https://platform.claude.com/docs/en/api/beta-headers#endpoint-specific-headers). ## Session statuses Sessions progress through these statuses. See [Start a session](https://platform.claude.com/docs/en/managed-agents/sessions) for the session lifecycle. | Status | Description | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `idle` | Agent is waiting for input, including user messages or tool confirmations. Sessions created without `initial_events` start in `idle`. | | `running` | Agent is actively executing. | | `rescheduling` | Transient error occurred, retrying automatically. | | `terminated` | Session has ended, either because of an unrecoverable error or because it was archived. A session that finishes its work goes `idle`, not `terminated`. | ## Updating the agent configuration You can update a session's `agent.tools` and `agent.mcp_servers`, including permission policies, mid-session without creating a new agent version. Updates are session-local and do not propagate back to the underlying agent. Only the agent's `tools` and `mcp_servers` can change after a session is created. To run a session with `model`, `system`, or `skills` values other than the agent's, use [agent configuration overrides](https://platform.claude.com/docs/en/managed-agents/sessions#override-agent-configuration-for-a-session) when you create the session. The agent's model configuration, including its [`inference_geo`](https://platform.claude.com/docs/en/manage-claude/data-residency) pin, also can't change mid-session: set the pin when you save the agent, or set or clear it for a single session with a `model` override when you create it. The agent's configured `system` field is fixed for the session's lifetime. On models that support it, you can still append system-level guidance mid-session by sending a [`system.message` event](https://platform.claude.com/docs/en/managed-agents/events-and-streaming#sending-system-messages). The semantics of a `tools` or `mcp_servers` update are full replacement: the provided array is the new value. To preserve existing entries, `GET` the session, modify the array, and `POST` it back. The session must be `idle` to update the agent. [Interrupt](https://platform.claude.com/docs/en/managed-agents/events-and-streaming#integrating-events) the session if you need to update the agent while it's running. ```bash cURL curl -sS --fail-with-body "https://api.anthropic.com/v1/sessions/$SESSION_ID" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d @- <beta->sessions->update( $session->id, agent: BetaManagedAgentsSessionAgentUpdate::with( tools: [ BetaManagedAgentsAgentToolset20260401Params::with(type: 'agent_toolset_20260401'), BetaManagedAgentsMCPToolsetParams::with(mcpServerName: 'linear', type: 'mcp_toolset'), ], mcpServers: [ BetaManagedAgentsURLMCPServerParams::with( name: 'linear', type: 'url', url: 'https://mcp.linear.app/sse', ), ], ), ); ``` ```ruby Ruby client.beta.sessions.update( session.id, agent: { tools: [ {type: :agent_toolset_20260401}, {type: :mcp_toolset, mcp_server_name: "linear"} ], mcp_servers: [ {type: :url, name: "linear", url: "https://mcp.linear.app/sse"} ] } ) ``` ## Updating the session budget A session [created with a budget](https://platform.claude.com/docs/en/managed-agents/sessions#set-a-session-budget) accepts two kinds of budget update: replacing the cap with a new `max_list_cost`, and removing it by setting `budget` to `null`. Both automatically resume work that paused when the session reached its cap. A replacement cap can be higher or lower than the current one, but it must be strictly greater than the session's consumed list cost, and removal is one-way: a non-null `budget` is accepted only on a session that currently has one, so you can't re-add a removed budget or add one to a session created without it. See [Session budgets](https://platform.claude.com/docs/en/managed-agents/budgets#resume-a-session-at-its-budget) for request examples, the error behaviors, and what counts toward list cost. ## Retrieving a session ```bash cURL retrieved=$(curl -fsSL "https://api.anthropic.com/v1/sessions/$SESSION_ID" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01") echo "Status: $(jq -r '.status' <<< "$retrieved")" ``` ```bash CLI ant beta:sessions retrieve --session-id "$SESSION_ID" ``` ```python Python retrieved = client.beta.sessions.retrieve(session.id) print(f"Status: {retrieved.status}") ``` ```typescript TypeScript const retrieved = await client.beta.sessions.retrieve(session.id); console.log(`Status: ${retrieved.status}`); ``` ```csharp C# var retrieved = await client.Beta.Sessions.Retrieve(session.ID); Console.WriteLine($"Status: {retrieved.Status.Raw()}"); ``` ```go Go retrieved, err := client.Beta.Sessions.Get(ctx, session.ID, anthropic.BetaSessionGetParams{}) if err != nil { panic(err) } fmt.Printf("Status: %s\n", retrieved.Status) ``` ```java Java var retrieved = client.beta().sessions().retrieve(session.id()); IO.println("Status: " + retrieved.status()); ``` ```php PHP $retrieved = $client->beta->sessions->retrieve($session->id); echo "Status: {$retrieved->status}\n"; ``` ```ruby Ruby retrieved = client.beta.sessions.retrieve(session.id) puts "Status: #{retrieved.status}" ``` ## Listing sessions Results from `GET /v1/sessions` are paginated. Use the `limit` query parameter to control the page size. Each response includes a `next_page` cursor; pass it as the `page` parameter on the next request to fetch the following page. `next_page` is `null` when there are no more results. To go back a page, pass `prev_page` as the `page` parameter. `prev_page` is `null` when you're on the first page. A `page` cursor is opaque and encodes the `order` of the request that produced it. The `order` query parameter sets the sort direction of the results, `asc` or `desc` by creation time; the default is `desc` (newest first). Reusing a cursor with a different `order` returns a 400 error, as does changing a `created_at` filter so that it excludes the cursor's position. Other query parameters, including the remaining filters and `limit`, can change between paginated requests. For the pagination fields shared across list endpoints, see [Pagination](https://platform.claude.com/docs/en/api/overview#pagination). ```bash cURL first_page=$(curl -sS --fail-with-body \ "https://api.anthropic.com/v1/sessions?agent_id=$AGENT_ID&limit=1" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01") jq '{prev_page, next_page}' <<< "$first_page" # prev_page is null on the first page next_cursor=$(jq -r '.next_page' <<< "$first_page") second_page=$(curl -sS --fail-with-body \ "https://api.anthropic.com/v1/sessions?agent_id=$AGENT_ID&limit=1&page=$next_cursor" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01") prev_cursor=$(jq -r '.prev_page' <<< "$second_page") curl -sS --fail-with-body \ "https://api.anthropic.com/v1/sessions?agent_id=$AGENT_ID&limit=1&page=$prev_cursor" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ | jq '{prev_page, next_page}' ``` ```bash CLI # --format raw returns one page envelope with its prev_page and next_page # cursors; the default output auto-paginates and emits only the sessions. cursors=$(ant beta:sessions list \ --agent-id "$AGENT_ID" \ --limit 1 \ --format raw \ --transform '{prev_page,next_page}') printf '%s\n' "$cursors" # Pass the next_page cursor back as --page to fetch the next page. NEXT_PAGE=$(jq -r '.next_page' <<< "$cursors") ant beta:sessions list \ --agent-id "$AGENT_ID" \ --limit 1 \ --page "$NEXT_PAGE" \ --format raw \ --transform '{prev_page,next_page}' # Pass that response's prev_page as --page to go back the same way. ``` ```python Python # Set `limit` low so the results span more than one page. first_page = client.beta.sessions.list(limit=1, agent_id=agent.id) # `prev_page` is None on the first page; `next_page` is None on the last. print(f"prev_page: {first_page.prev_page}") print(f"next_page: {first_page.next_page}") # Pass `next_page` back as `page` to fetch the next page. second_page = client.beta.sessions.list( limit=1, agent_id=agent.id, page=first_page.next_page ) for listed_session in second_page.data: print(f"{listed_session.id}: {listed_session.status}") # Pass `prev_page` back as `page` to return to the previous page. previous_page = client.beta.sessions.list( limit=1, agent_id=agent.id, page=second_page.prev_page ) for listed_session in previous_page.data: print(f"{listed_session.id}: {listed_session.status}") # For forward-only iteration, the page object is also directly iterable. ``` ```typescript TypeScript const firstPage = await client.beta.sessions.list({ limit: 1, agent_id: agent.id }); // prev_page is null on the first page; next_page is set when more sessions exist. console.log(`prev_page: ${firstPage.prev_page}`); console.log(`next_page: ${firstPage.next_page}`); // Pass next_page as the `page` cursor to fetch the second page. const secondPage = await client.beta.sessions.list({ limit: 1, agent_id: agent.id, page: firstPage.next_page }); for (const listedSession of secondPage.data) { console.log(`Page 2 has ${listedSession.id}: ${listedSession.status}`); } // Pass the second page's prev_page cursor to step back to the first page. const previousPage = await client.beta.sessions.list({ limit: 1, agent_id: agent.id, page: secondPage.prev_page }); for (const listedSession of previousPage.data) { console.log(`Back on page 1: ${listedSession.id} is ${listedSession.status}`); } // For forward-only iteration, the page object is also directly iterable. ``` ```csharp C# // The SessionListPage that `List` returns exposes the items but not the // pagination cursors. To read `prev_page` / `next_page`, deserialize the raw // response into SessionListPageResponse instead. using var page1Response = await client.Beta.Sessions.WithRawResponse.List( new SessionListParams { Limit = 1, AgentID = agent.ID } ); var page1 = await page1Response.Deserialize(); Console.WriteLine($"prev_page: {page1.PrevPage ?? "null"}"); Console.WriteLine($"next_page: {page1.NextPage ?? "null"}"); // Advance: pass `next_page` from page 1 as the `page` cursor. using var page2Response = await client.Beta.Sessions.WithRawResponse.List( new SessionListParams { Limit = 1, AgentID = agent.ID, Page = page1.NextPage } ); var page2 = await page2Response.Deserialize(); foreach (var listedSession in page2.Data ?? []) { Console.WriteLine($"Page 2: {listedSession.ID}: {listedSession.Status.Raw()}"); } // Go back: pass `prev_page` from page 2 as the same `page` cursor. using var previousPageResponse = await client.Beta.Sessions.WithRawResponse.List( new SessionListParams { Limit = 1, AgentID = agent.ID, Page = page2.PrevPage } ); var previousPage = await previousPageResponse.Deserialize(); foreach (var listedSession in previousPage.Data ?? []) { Console.WriteLine($"Back to page 1: {listedSession.ID}: {listedSession.Status.Raw()}"); } // For forward-only iteration, (await client.Beta.Sessions.List(...)).Paginate() returns an IAsyncEnumerable that auto-follows next_page. ``` ```go Go // Page 1: prev_page is empty because nothing precedes the first page. firstPage, err := client.Beta.Sessions.List(ctx, anthropic.BetaSessionListParams{ AgentID: anthropic.String(agent.ID), Limit: anthropic.Int(1), }) if err != nil { panic(err) } fmt.Printf("Page 1 prev_page: %q\n", firstPage.PrevPage) fmt.Printf("Page 1 next_page: %q\n", firstPage.NextPage) // Advance: pass next_page as the Page cursor to fetch page 2. secondPage, err := client.Beta.Sessions.List(ctx, anthropic.BetaSessionListParams{ AgentID: anthropic.String(agent.ID), Limit: anthropic.Int(1), Page: anthropic.String(firstPage.NextPage), }) if err != nil { panic(err) } for _, listedSession := range secondPage.Data { fmt.Printf("Page 2: %s: %s\n", listedSession.ID, listedSession.Status) } // Go back: page 2's prev_page is the cursor for the page before it. previousPage, err := client.Beta.Sessions.List(ctx, anthropic.BetaSessionListParams{ AgentID: anthropic.String(agent.ID), Limit: anthropic.Int(1), Page: anthropic.String(secondPage.PrevPage), }) if err != nil { panic(err) } for _, listedSession := range previousPage.Data { fmt.Printf("Back to page 1: %s: %s\n", listedSession.ID, listedSession.Status) } // For forward-only iteration, use ListAutoPaging to auto-follow next_page. ``` ```java Java var params = SessionListParams.builder() .agentId(agent.id()) .limit(1) .build(); var firstPage = client.beta().sessions().list(params); for (var listedSession : firstPage.data()) { IO.println(listedSession.id() + ": " + listedSession.status()); } // prev_page is an empty Optional on the first page; next_page points to page 2. IO.println("prev_page: " + firstPage.response().prevPage()); IO.println("next_page: " + firstPage.response().nextPage()); // Advance by passing next_page as the page cursor. var nextCursor = firstPage.response().nextPage().orElseThrow(); var secondPage = client.beta().sessions().list(params.toBuilder().page(nextCursor).build()); // Go back by passing prev_page as the same page cursor. var prevCursor = secondPage.response().prevPage().orElseThrow(); var previousPage = client.beta().sessions().list(params.toBuilder().page(prevCursor).build()); // Back on the first page, so prev_page is empty again. IO.println("prev_page: " + previousPage.response().prevPage()); // For forward-only iteration, page.autoPager() returns an Iterable that auto-follows next_page. ``` ```php PHP // Page 1: prevPage is null because nothing precedes the first page. $firstPage = $client->beta->sessions->list(agentID: $agent->id, limit: 1); echo 'Page 1 prev_page: ' . ($firstPage->prevPage ?? 'null') . "\n"; echo 'Page 1 next_page: ' . ($firstPage->nextPage ?? 'null') . "\n"; // Advance: pass nextPage back as the `page` cursor to fetch page 2. $secondPage = $client->beta->sessions->list( agentID: $agent->id, limit: 1, page: $firstPage->nextPage, ); foreach ($secondPage->getItems() as $listedSession) { echo "Page 2: {$listedSession->id}: {$listedSession->status}\n"; } // Go back: page 2's prevPage is the cursor for the page before it. $previousPage = $client->beta->sessions->list( agentID: $agent->id, limit: 1, page: $secondPage->prevPage, ); foreach ($previousPage->getItems() as $listedSession) { echo "Back to page 1: {$listedSession->id}: {$listedSession->status}\n"; } // For forward-only iteration, $page->pagingEachItem() yields every session across pages. ``` ```ruby Ruby first_page = client.beta.sessions.list(agent_id: agent.id, limit: 1) first_page.data.each do |listed_session| puts "#{listed_session.id}: #{listed_session.status}" end # `prev_page` is nil on the first page. The next-page cursor is exposed as # `next_page_` (trailing underscore) because plain `next_page` is the helper # method that fetches the next page object for you. puts "prev_page: #{first_page.prev_page.inspect}" puts "next_page: #{first_page.next_page_.inspect}" # Pass either cursor back as `page` to move through the list in both directions. second_page = client.beta.sessions.list( agent_id: agent.id, limit: 1, page: first_page.next_page_ ) back_to_first = client.beta.sessions.list( agent_id: agent.id, limit: 1, page: second_page.prev_page ) back_to_first.data.each do |listed_session| puts "#{listed_session.id}: #{listed_session.status}" end # For forward-only iteration, page.auto_paging_each auto-follows next_page. ``` ## Archiving a session Archive a session to prevent new events from being sent while preserving its history. A `running` session cannot be archived; send an [interrupt event](https://platform.claude.com/docs/en/managed-agents/events-and-streaming#integrating-events) if you need to archive it immediately. ```bash cURL curl -fsSL -X POST "https://api.anthropic.com/v1/sessions/$SESSION_ID/archive" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" ``` ```bash CLI ant beta:sessions archive \ --session-id "$SESSION_ID" ``` ```python Python client.beta.sessions.archive(session.id) ``` ```typescript TypeScript await client.beta.sessions.archive(session.id); ``` ```csharp C# await client.Beta.Sessions.Archive(session.ID); ``` ```go Go _, err = client.Beta.Sessions.Archive(ctx, session.ID, anthropic.BetaSessionArchiveParams{}) if err != nil { panic(err) } ``` ```java Java client.beta().sessions().archive(session.id()); ``` ```php PHP $client->beta->sessions->archive($session->id); ``` ```ruby Ruby client.beta.sessions.archive(session.id) ``` ## Deleting a session Delete a session to permanently remove its record, events, and associated sandbox. A `running` session cannot be deleted; send an [interrupt event](https://platform.claude.com/docs/en/managed-agents/events-and-streaming#integrating-events) if you need to delete it immediately. Memory stores, vaults, skills, environments, and agents are independent resources and are not affected by session deletion. Files you uploaded through the Files API are also unaffected, but files the session itself produced are scoped to it and are permanently deleted along with its filesystem. Download anything you need to keep before deleting the session. ```bash cURL curl -fsSL -X DELETE "https://api.anthropic.com/v1/sessions/$SESSION_ID" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" ``` ```bash CLI ant beta:sessions delete \ --session-id "$SESSION_ID" ``` ```python Python client.beta.sessions.delete(session.id) ``` ```typescript TypeScript await client.beta.sessions.delete(session.id); ``` ```csharp C# await client.Beta.Sessions.Delete(session.ID); ``` ```go Go _, err = client.Beta.Sessions.Delete(ctx, session.ID, anthropic.BetaSessionDeleteParams{}) if err != nil { panic(err) } ``` ```java Java client.beta().sessions().delete(session.id()); ``` ```php PHP $client->beta->sessions->delete($session->id); ``` ```ruby Ruby client.beta.sessions.delete(session.id) ``` --- title: Start a session url: https://platform.claude.com/docs/en/managed-agents/sessions description: Create a session to run your agent and begin executing tasks. --- A session is an agent instance within an environment. Each session references an [agent](https://platform.claude.com/docs/en/managed-agents/agent-setup) and an [environment](https://platform.claude.com/docs/en/managed-agents/environments) (both created separately), and maintains conversation history across multiple interactions. Sessions follow a two-step lifecycle: first [create the session](https://platform.claude.com/docs/en/managed-agents/sessions#creating-a-session), then [send a user event](https://platform.claude.com/docs/en/managed-agents/sessions#starting-the-session) to start work. You can also collapse both steps into one call with [`initial_events`](https://platform.claude.com/docs/en/managed-agents/sessions#seed-the-session-with-initial-events). Managed Agents API requests require the `managed-agents-2026-04-01` beta header, except memory store endpoints, which use `agent-memory-2026-07-22` instead. The SDK sets the correct beta header automatically. See [Beta headers](https://platform.claude.com/docs/en/api/beta-headers#endpoint-specific-headers). ## Creating a session A session requires an `agent` ID and an `environment` ID. Agents are versioned resources; passing in the `agent` ID as a string creates the session with the latest agent version. ```bash cURL session=$(curl -fsSL https://api.anthropic.com/v1/sessions \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d @- <beta->sessions->create( agent: $agent->id, environmentID: $environment->id, ); ``` ```ruby Ruby session = client.beta.sessions.create( agent: agent.id, environment_id: environment.id ) ``` To pin a session to a specific agent version, pass an object. This lets you control exactly which version runs and stage rollouts of new versions independently. ```bash cURL pinned_session=$(curl -fsSL https://api.anthropic.com/v1/sessions \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d @- <beta->sessions->create( agent: ['type' => 'agent', 'id' => $agent->id, 'version' => 1], environmentID: $environment->id, ); ``` ```ruby Ruby pinned_session = client.beta.sessions.create( agent: {type: :agent, id: agent.id, version: 1}, environment_id: environment.id ) ``` ### Seed the session with initial events You can create a session and start its work in one call. `initial_events` is an optional array of initial [events](https://platform.claude.com/docs/en/managed-agents/reference#event-types) to send to the session at creation, processed in order. It supports `user.message` and [`user.define_outcome`](https://platform.claude.com/docs/en/managed-agents/define-outcomes) events, and accepts a maximum of 50 events. A non-empty list starts the agent loop in the same call: the session is created directly in the `running` status, with no further request. The following example creates a session with a single `user.message` in `initial_events`: ```bash cURL seeded_session=$(curl -fsSL https://api.anthropic.com/v1/sessions \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d @- <beta->sessions->create( agent: $agent->id, environmentID: $environment->id, initialEvents: [ [ 'type' => 'user.message', 'content' => [['type' => 'text', 'text' => 'List the files in the working directory.']], ], ], ); // initial_events are not echoed on the create response; read them back // from the session's event list. $seededEvents = $client->beta->sessions->events->list($seededSession->id); foreach ($seededEvents->getItems() as $event) { if ($event->type === 'user.message') { echo "Seeded event: {$event->content[0]->text}\n"; } } ``` ```ruby Ruby seeded_session = client.beta.sessions.create( agent: agent.id, environment_id: environment.id, initial_events: [ { type: :"user.message", content: [{type: :text, text: "List the files in the working directory."}] } ] ) # initial_events are not echoed on the create response; read them back from # the session's event list. client.beta.sessions.events.list(seeded_session.id).auto_paging_each do |event| next unless event.type == :"user.message" event.content.each do |block| puts "Seeded event: #{block.text}" if block.type == :text end end ``` No other event type is accepted. Events that respond to an agent turn (`user.tool_confirmation`, `user.tool_result`, and `user.custom_tool_result`) aren't accepted because no agent turn exists yet, and `user.interrupt` isn't accepted because there is no turn to stop. Unlike `initial_events` on a scheduled deployment, a session's `initial_events` don't accept `system.message`. Each event in `initial_events` is validated and persisted before the create response returns, in list order, with a server-assigned ID, exactly as if you had posted it to the [send events](https://platform.claude.com/docs/en/managed-agents/events-and-streaming) endpoint immediately after creation. Per-event content rules are also the same as on that endpoint. An empty list is equivalent to omitting the field. Validation is all-or-nothing: if any event fails validation, the whole request is rejected and no session is created. The create request is rejected in the following cases: | Condition | Status | | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | | More than one `user.define_outcome` event | 400 | | A `user.define_outcome` event without a `rubric` | 400 | | More than 100 file-sourced [`document` content blocks](https://platform.claude.com/docs/en/build-with-claude/files#document-blocks) across the whole list | 400 | | A request body over 32 MB | 413 | A `user.define_outcome` event in `initial_events` is accepted under the same conditions as sending one to an existing session; see [Define outcomes](https://platform.claude.com/docs/en/managed-agents/define-outcomes). ### Override agent configuration for a session You can pass `agent` in three forms: an agent ID string, a pinned-version object (`type: "agent"`), or an overrides object. The overrides form changes parts of the agent's configuration for a single session. Use it to try a different model or grant an extra tool in one session without versioning the agent. For the overrides form, set `type` to `agent_with_overrides` and pass the agent's `id` and optionally a `version` (omit `version` to use the agent's latest version). Then include any of `model`, `system`, `tools`, `mcp_servers`, or `skills` with the values the session should use. Each overridable field follows the same three rules: * **Omit the field:** The session inherits the value from the agent version it references. * **Set the field to `null`, or to an empty array for list fields:** The session runs with that field cleared. This rule applies in full to `system` and `skills`. There are three exceptions: * `model` is never clearable. A session always needs a model, so `model: null` returns a 400 `agent_model_required` error. * Clearing `tools` returns a 400 error when the session's effective `skills` is non-empty, because skills require the `read` tool. Otherwise, `tools: null` and `tools: []` clear the field. * Clearing `mcp_servers` returns a 400 error when the session's effective `tools` still contains an `mcp_toolset` that references one of the agent's servers. Override `tools` in the same request to remove those `mcp_toolset` entries, then clear `mcp_servers`. * **Set the field to a value:** The value replaces the agent's value in full. Overrides never merge with the agent's configuration, so a `tools` override must list every tool the session should have. There is one exception: * An `effort` level inside a per-session `model` override isn't applied, and because the override replaces the agent's `model` object in full, the agent's own `effort` isn't carried over either: a session created with a `model` override runs at the model's default effort level. To run at a specific effort level, set `effort` on the [agent](https://platform.claude.com/docs/en/managed-agents/agent-setup#agent-configuration-fields) and don't override `model` for that session. Overrides apply only to the session you create. They do not modify the agent resource or create a new agent version, so other sessions that reference the same agent are unaffected. In the response, the `agent` object reflects the configuration the session runs with after the overrides are applied. Its `id` and `version` still identify the agent and version the overrides are applied to. This lets you trace a session back to its base agent. The following example starts a session that overrides the model and clears the system prompt: ```bash cURL override_session=$(curl -fsSL https://api.anthropic.com/v1/sessions \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d @- <id, type: 'agent_with_overrides', model: ['id' => 'claude-sonnet-5'], ); // Clear the system prompt for this session. Array access is load-bearing here: // create() strips nulls from raw arrays and ::with() treats null args as omitted. $overrides['system'] = null; $overrideSession = $client->beta->sessions->create( agent: $overrides, environmentID: $environment->id, ); // The response's agent is the resolved snapshot with the overrides applied. echo "Model: {$overrideSession->agent->model->id}\n"; echo 'System: ' . ($overrideSession->agent->system ?? 'null') . "\n"; ``` ```ruby Ruby # The system prompt override is `system_` (trailing underscore) because plain # `system` is Ruby's Kernel#system. Setting it to nil clears the prompt. override_session = client.beta.sessions.create( agent: Anthropic::Beta::BetaManagedAgentsAgentWithOverridesParams.new( type: :agent_with_overrides, id: agent.id, model: {id: "claude-sonnet-5"}, system_: nil ), environment_id: environment.id ) # The response's agent is the resolved snapshot with the overrides applied. puts "Model: #{override_session.agent.model.id}" puts "System: #{override_session.agent.system_.inspect}" ``` #### Pin the inference geo for a session Because a `model` override replaces the agent's `model` object in full, it also sets or clears the model's [`inference_geo`](https://platform.claude.com/docs/en/manage-claude/data-residency) pin for the session: an override that includes `inference_geo` pins the geography that serves the session's model requests, and one that omits it clears the agent's pin so the session follows the workspace's `default_inference_geo`. The overridden value is validated against the workspace's `allowed_inference_geos` when the session is created. The following example starts a session from an agent whose model has no geo pin, pins the session's model requests to US inference by including `inference_geo` in the `model` override, and prints the value echoed in the response's `agent.model`: ```bash cURL # Replaces the agent's `model` in full: restate `id`, add `inference_geo` to pin. session=$(curl -fsSL https://api.anthropic.com/v1/sessions \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d @- <beta->sessions->create( agent: BetaManagedAgentsAgentWithOverridesParams::with( id: $agent->id, type: 'agent_with_overrides', // Replaces the agent's `model` in full: restate `id`, add `inference_geo` to pin. model: BetaManagedAgentsModelConfigParams::with( id: 'claude-opus-5', inferenceGeo: 'us', ), ), environmentID: $environment->id, ); echo "Inference geo: {$session->agent->model->inferenceGeo}\n"; ``` ```ruby Ruby session = client.beta.sessions.create( agent: { type: :agent_with_overrides, id: agent.id, # Replaces the agent's `model` in full: restate `id`, add `inference_geo` to pin. model: {id: "claude-opus-5", inference_geo: "us"} }, environment_id: environment.id ) puts "Inference geo: #{session.agent.model.inference_geo}" ``` The agent defines how Claude behaves within the session, including the model, system prompt, tools, and MCP servers. See [Define your agent](https://platform.claude.com/docs/en/managed-agents/agent-setup) for details. ### Set a session budget To cap what a session can spend, pass the optional `budget` object when you create it. A budget is a hard ceiling on the session's list cost: the platform prices everything the session consumes at public list rates, and the session stops issuing new model requests once that running total reaches `max_list_cost`. Set `type` to `limit` and give `max_list_cost` an `amount` and a `currency`. `amount` is a whole number of US cents written as a string, such as `"2500"` for $25.00; the API takes a string rather than a number so no floating-point rounding is ever applied. `USD` is the only currency currently supported. When the session reaches the cap, it pauses and goes idle with the stop reason `budget_reached`. The cap is enforced between model requests, so the request that crosses it finishes first and the session's final list cost can land [a fraction past the cap](https://platform.claude.com/docs/en/managed-agents/budgets#when-a-session-reaches-its-budget). A budget can only be attached at creation: you can [change or remove](https://platform.claude.com/docs/en/managed-agents/session-operations#updating-the-session-budget) it later, but you can't add one to a session created without it. The following example creates a session with a $25.00 budget; the response echoes the `budget` on the session resource: ```bash cURL curl -fsSL https://api.anthropic.com/v1/sessions \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d @- < ```bash cURL vault_session=$(curl -fsSL https://api.anthropic.com/v1/sessions \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d @- <beta->sessions->create( agent: $agent->id, environmentID: $environment->id, vaultIDs: [$vault->id], ); ``` ```ruby Ruby vault_session = client.beta.sessions.create( agent: agent.id, environment_id: environment.id, vault_ids: [vault.id] ) ```
## Starting the session Creating a session without `initial_events` registers the session but does not start any work; the environment's sandbox begins provisioning as soon as the session is created, so the first tool call does not wait on it. To delegate a task, send events to the session using a [user event](https://platform.claude.com/docs/en/managed-agents/reference#event-types). To supply the first event in the create request instead, see [Seed the session with initial events](https://platform.claude.com/docs/en/managed-agents/sessions#seed-the-session-with-initial-events). The session acts as a state machine that tracks progress while events drive the actual execution. ```bash cURL curl -fsSL "https://api.anthropic.com/v1/sessions/$SESSION_ID/events" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d @- <<'EOF' { "events": [ { "type": "user.message", "content": [{"type": "text", "text": "List the files in the working directory."}] } ] } EOF ``` ```bash CLI ant beta:sessions:events send \ --session-id "$SESSION_ID" <<'YAML' events: - type: user.message content: - type: text text: List the files in the working directory. YAML ``` ```python Python client.beta.sessions.events.send( session.id, events=[ { "type": "user.message", "content": [ {"type": "text", "text": "List the files in the working directory."} ], }, ], ) ``` ```typescript TypeScript await client.beta.sessions.events.send(session.id, { events: [ { type: "user.message", content: [{ type: "text", text: "List the files in the working directory." }] } ] }); ``` ```csharp C# await client.Beta.Sessions.Events.Send(session.ID, new() { Events = [ new BetaManagedAgentsUserMessageEventParams { Type = BetaManagedAgentsUserMessageEventParamsType.UserMessage, Content = [ new BetaManagedAgentsTextBlock { Type = BetaManagedAgentsTextBlockType.Text, Text = "List the files in the working directory.", }, ], }, ], }); ``` ```go Go if _, err := client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{ Events: []anthropic.BetaManagedAgentsEventParamsUnion{{ OfUserMessage: &anthropic.BetaManagedAgentsUserMessageEventParams{ Type: anthropic.BetaManagedAgentsUserMessageEventParamsTypeUserMessage, Content: []anthropic.BetaManagedAgentsUserMessageEventParamsContentUnion{{ OfText: &anthropic.BetaManagedAgentsTextBlockParam{ Type: anthropic.BetaManagedAgentsTextBlockTypeText, Text: "List the files in the working directory.", }, }}, }, }}, }); err != nil { panic(err) } ``` ```java Java client.beta().sessions().events().send( session.id(), EventSendParams.builder() .addEvent(BetaManagedAgentsUserMessageEventParams.builder() .type(BetaManagedAgentsUserMessageEventParams.Type.USER_MESSAGE) .addTextContent("List the files in the working directory.") .build()) .build()); ``` ```php PHP $client->beta->sessions->events->send( $session->id, events: [ [ 'type' => 'user.message', 'content' => [['type' => 'text', 'text' => 'List the files in the working directory.']], ], ], ); ``` ```ruby Ruby client.beta.sessions.events.send_( session.id, events: [ { type: :"user.message", content: [{type: :text, text: "List the files in the working directory."}] } ] ) ``` See [Session event stream](https://platform.claude.com/docs/en/managed-agents/events-and-streaming) for how to stream the agent's responses and handle tool confirmations. See [Session statuses](https://platform.claude.com/docs/en/managed-agents/session-operations#session-statuses) for the statuses a session moves through. ## Next steps Retrieve, list, update, archive, and delete Claude Managed Agents sessions. Send events, stream responses, and interrupt or redirect your session mid-execution. Create and manage deployments with the Claude API: run an agent on a recurring cron schedule and inspect its run history. --- title: Subscribe to webhooks url: https://platform.claude.com/docs/en/managed-agents/webhooks description: Get notified when major events happen without polling. --- Sessions are long-running interactions. While most real-time interactions happen through the [SSE event stream](https://platform.claude.com/docs/en/managed-agents/events-and-streaming), webhooks notify you of major state changes. Webhook events return the event `type` and `id`, not the full object. When you receive a webhook event, you need to fetch the object directly with a `GET` call. This avoids delivering stale data on retries and keeps every delivery small. ## Supported event types | Event | Trigger | | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `session.status_run_started` | Agent execution started. This triggers at every session status transition to `running`. | | `session.status_idled` | Agent awaiting input, for example, a tool permission approval or a new user message. | | `session.budget_reached` | The session reached its [budget](https://platform.claude.com/docs/en/managed-agents/budgets) and paused. Fires at most once for each budget value you set; changing the budget arms it again. | | `session.status_rescheduled` | A transient error occurred and the session is retrying automatically. | | `session.status_terminated` | The session terminated, either because of an unrecoverable error or because it was archived. | | `session.thread_created` | New [multiagent thread](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration) opened: an additional agent called by the coordinator is starting work, or the session's [advisor](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration#give-the-session-an-advisor) is being consulted. | | `session.thread_idled` | An agent in a [multiagent interaction](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration) is waiting for input. | | `session.thread_terminated` | A [multiagent thread](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration) terminated, either because the thread was archived or because it exhausted its retries. A coordinator-spawned child that finishes its work goes `idle`, not `terminated` (an advisor thread terminates once its consultation completes). Fires for child threads only; the primary thread's end, including archiving the whole session, surfaces only as `session.status_terminated`. | | `session.outcome_evaluation_ended` | [Outcome evaluation](https://platform.claude.com/docs/en/managed-agents/define-outcomes) for a single iteration completed. | | `session.updated` | Session properties changed (for example, its name or configuration was updated). | | `session.deleted` | Session permanently deleted. There is no object left to fetch, so treat the event itself as final. | | Event | Trigger | | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `vault.created` | Vault created. | | `vault.archived` | Vault archived. A `vault_credential.archived` event is also emitted for each underlying credential. | | `vault.deleted` | Vault deleted. A `vault_credential.deleted` event is also emitted for each underlying credential. There is no object left to fetch, so treat the event itself as final. | | `vault_credential.created` | Credential created. | | `vault_credential.archived` | Credential archived, either directly or as a result of vault archival. | | `vault_credential.deleted` | Credential deleted, either directly or as a result of vault deletion. There is no object left to fetch, so treat the event itself as final. | | `vault_credential.refresh_failed` | An `mcp_oauth` credential cannot be refreshed (invalid refresh token, or irrecoverable error from the OAuth server). | These events track the lifecycle of the agent resources in your workspace, and are distinct from the agent events delivered on a session's event stream. | Event | Trigger | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `agent.created` | Agent created. | | `agent.updated` | A [new version of the agent](https://platform.claude.com/docs/en/managed-agents/agent-setup#update-an-agent) was published. Updates that do not create a new version do not trigger this event. | | `agent.archived` | Agent archived. | | `agent.deleted` | Agent permanently deleted. There is no object left to fetch, so treat the event itself as final. | | Event | Trigger | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `deployment.created` | [Scheduled deployment](https://platform.claude.com/docs/en/managed-agents/scheduled-deployments) created. | | `deployment.updated` | Deployment properties changed (for example, its schedule was updated). | | `deployment.paused` | Deployment paused, either by request or automatically when a scheduled run fails with an unrecoverable error, such as an archived subagent or an archived environment. Recoverable failures, including rate limits, don't pause the deployment. See [Failure behavior](https://platform.claude.com/docs/en/managed-agents/scheduled-deployments#failure-behavior). | | `deployment.unpaused` | Deployment unpaused, resuming its schedule. | | `deployment.archived` | Deployment archived, either directly or because its agent was archived. If the agent is deleted instead, a scheduled deployment is archived at its next scheduled run; a deployment without a schedule is not archived automatically. | | `deployment.deleted` | Deployment permanently deleted. There is no object left to fetch, so treat the event itself as final. | | Event | Trigger | | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `deployment_run.started` | A scheduled run started. Only scheduled runs emit `deployment_run` events; [manual runs](https://platform.claude.com/docs/en/managed-agents/scheduled-deployments#trigger-a-manual-run) do not. | | `deployment_run.succeeded` | A scheduled run created its session. The event carries the same `data.id` (the run ID) as the run's `deployment_run.started` event. To follow the session's work, subscribe to its session events (the Session events tab), or fetch the [deployment run](https://platform.claude.com/docs/en/managed-agents/scheduled-deployments#deployment-runs) for its `session_id`. | | `deployment_run.failed` | A scheduled run did not create a session. The event carries the same `data.id` as the run's `deployment_run.started` event. Fetch the [deployment run](https://platform.claude.com/docs/en/managed-agents/scheduled-deployments#deployment-runs) for the error details. | | Event | Trigger | | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `environment.created` | Environment created. | | `environment.updated` | Environment updated with at least one changed field. A no-op update emits nothing. | | `environment.archived` | Environment archived. Re-archiving an already-archived environment emits nothing. | | `environment.deleted` | Environment deleted, including delete of an already-archived environment. There is no object left to fetch, so treat the event itself as final. | An environment's [work items](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes) emit no webhook events. | Event | Trigger | | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `memory_store.created` | Memory store created, either by you or by an Anthropic-operated process that clones one of your existing stores. | | `memory_store.archived` | Memory store archived. Re-archiving an already-archived store emits nothing. | | `memory_store.deleted` | Memory store deleted, including delete of an already-archived store. Deleting a store cascades to its memories and memory versions without emitting per-memory events; the single `memory_store.deleted` event is the signal. There is no object left to fetch, so treat the event itself as final. | Individual [memories](https://platform.claude.com/docs/en/managed-agents/memory) and memory versions emit no webhook events. ## Register an endpoint Visit **Manage > Webhooks** in the [Claude Console](https://platform.claude.com/settings/workspaces/default/webhooks). A webhook endpoint consists of: * **URL:** Must be HTTPS on port 443 with a publicly resolvable hostname. * **Event types:** The list of `data.type` values this endpoint receives. An endpoint only receives events it's subscribed to. * **Signing secret:** A 32-byte `whsec_`-prefixed secret generated at creation. It's shown only once, so store it securely to verify webhook deliveries. ## Verify the signature Every delivery carries the `webhook-id`, `webhook-timestamp`, and `webhook-signature` headers. Use the SDK's `unwrap()` helper to verify the signature and parse the event in one step. It throws if the signature is invalid or the payload is more than 5 minutes old. Set `ANTHROPIC_WEBHOOK_SIGNING_KEY` to the `whsec_`-prefixed secret shown at endpoint creation. ```python Python from flask import Flask, request import anthropic client = anthropic.Anthropic() # reads ANTHROPIC_WEBHOOK_SIGNING_KEY from env app = Flask(__name__) @app.route("/webhook", methods=["POST"]) def webhook(): try: # unwrap() raises if the signature is invalid or the payload is stale event = client.beta.webhooks.unwrap( request.get_data(as_text=True), headers=dict(request.headers), ) except Exception: return "invalid signature", 400 if event.data.type == "session.status_idled": print("session idled:", event.data.id) # handle other event types return "", 200 ``` ```typescript TypeScript import express from "express"; import Anthropic from "@anthropic-ai/sdk"; const client = new Anthropic(); // reads ANTHROPIC_WEBHOOK_SIGNING_KEY from env const app = express(); // IMPORTANT: use express.raw(), not express.json(). The signature is computed over raw bytes. app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => { let event; try { // unwrap() throws if the signature is invalid or the payload is stale event = client.beta.webhooks.unwrap(req.body.toString("utf8"), { headers: req.headers as Record }); } catch { return res.status(400).send("invalid signature"); } switch (event.data.type) { case "session.status_idled": console.log("session idled:", event.data.id); break; // handle other event types } res.sendStatus(200); }); ``` ```csharp C# using Anthropic; var client = new AnthropicClient(); // reads ANTHROPIC_WEBHOOK_SIGNING_KEY from env var app = WebApplication.Create(args); app.MapPost("/webhook", async (HttpRequest request) => { using var reader = new StreamReader(request.Body); var body = await reader.ReadToEndAsync(); var headers = request.Headers.ToDictionary(header => header.Key, header => header.Value.ToString()); UnwrapWebhookEvent webhookEvent; try { // Unwrap() throws if the signature is invalid or the payload is stale webhookEvent = client.Beta.Webhooks.Unwrap(body, headers); } catch { return Results.BadRequest("invalid signature"); } if (webhookEvent.Data.TryPickSessionStatusIdled(out var idled)) { Console.WriteLine($"session idled: {idled.ID}"); } // handle other event types return Results.Ok(); }); ``` ```go Go package main import ( "fmt" "io" "net/http" "github.com/anthropics/anthropic-sdk-go" ) var client = anthropic.NewClient() // reads ANTHROPIC_WEBHOOK_SIGNING_KEY from env func webhook(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) if err != nil { http.Error(w, "could not read body", http.StatusBadRequest) return } // Unwrap returns an error if the signature is invalid or the payload is stale event, err := client.Beta.Webhooks.Unwrap(body, r.Header) if err != nil { http.Error(w, "invalid signature", http.StatusBadRequest) return } switch event.Data.Type { case "session.status_idled": fmt.Println("session idled:", event.Data.ID) // handle other event types } w.WriteHeader(http.StatusOK) } func main() { http.HandleFunc("/webhook", webhook) } ``` ```java Java import com.anthropic.client.AnthropicClient; import com.anthropic.client.okhttp.AnthropicOkHttpClient; import com.anthropic.core.UnwrapWebhookParams; import com.anthropic.core.http.Headers; import com.sun.net.httpserver.HttpServer; // reads ANTHROPIC_WEBHOOK_SIGNING_KEY from env AnthropicClient client = AnthropicOkHttpClient.fromEnv(); void main() throws Exception { var server = HttpServer.create(new InetSocketAddress(8000), 0); server.createContext("/webhook", exchange -> { var body = new String(exchange.getRequestBody().readAllBytes()); var headers = Headers.builder(); exchange.getRequestHeaders().forEach(headers::put); try { // unwrap() throws if the signature is invalid or the payload is stale var event = client.beta().webhooks().unwrap( UnwrapWebhookParams.builder() .body(body) .headers(headers.build()) .build()); event.data().sessionStatusIdled().ifPresent(idled -> IO.println("session idled: " + idled.id())); // handle other event types exchange.sendResponseHeaders(200, -1); } catch (Exception _) { exchange.sendResponseHeaders(400, -1); } exchange.close(); }); } ``` ```php PHP use Anthropic\Client; use Anthropic\Core\Exceptions\WebhookException; $client = new Client(); // reads ANTHROPIC_WEBHOOK_SIGNING_KEY from env $body = file_get_contents('php://input'); $headers = getallheaders(); try { // unwrap() throws if the signature is invalid or the payload is stale $event = $client->beta->webhooks->unwrap($body, headers: $headers); } catch (WebhookException) { http_response_code(400); exit('invalid signature'); } match ($event->data->type) { 'session.status_idled' => print "session idled: {$event->data->id}\n", // handle other event types default => null, }; http_response_code(200); ``` ```ruby Ruby require "sinatra" require "anthropic" client = Anthropic::Client.new # reads ANTHROPIC_WEBHOOK_SIGNING_KEY from env post "/webhook" do headers = request.env .select { |key, _| key.start_with?("HTTP_") } .transform_keys { it.delete_prefix("HTTP_").downcase.tr("_", "-") } begin # unwrap raises if the signature is invalid or the payload is stale event = client.beta.webhooks.unwrap(request.body.read, headers: headers) rescue StandardError halt 400, "invalid signature" end if event.data.type == :"session.status_idled" puts "session idled: #{event.data.id}" end # handle other event types status 200 end ``` ## Handle an event Parse the body, switch on `data.type`, and fetch the resource by ID. Return any `2xx` to acknowledge. Any other response counts against the endpoint: a `3xx` disables it immediately (redirects are never followed), while other failures are retried; see [Delivery behavior](https://platform.claude.com/docs/en/managed-agents/webhooks#delivery-behavior) for the retry and auto-disable rules. Every event payload has the same structure, including the event type, identifier, and the timestamp of when the event occurred. ```json { "type": "event", "id": "whe_9d5c1f7e...", "created_at": "2026-03-18T14:05:22Z", "data": { "type": "session.status_idled", "id": "sesn_01XYZ...", "organization_id": "8a3d2f1e-...", "workspace_id": "c7b0e4d9-..." } } ``` ```python Python if event.data.type == "session.status_idled": session = client.beta.sessions.retrieve(event.data.id) notify_user(session) return "", 204 ``` ```typescript TypeScript if (event.data.type === "session.status_idled") { const session = await client.beta.sessions.retrieve(event.data.id); notifyUser(session); } res.sendStatus(204); ``` ```csharp C# if (webhookEvent.Data.TryPickSessionStatusIdled(out var idled)) { var session = await client.Beta.Sessions.Retrieve(idled.ID); NotifyUser(session); } return Results.StatusCode(204); ``` ```go Go if event.Data.Type == "session.status_idled" { session, err := client.Beta.Sessions.Get(r.Context(), event.Data.ID, anthropic.BetaSessionGetParams{}) if err != nil { panic(err) } notifyUser(session) } w.WriteHeader(http.StatusNoContent) ``` ```java Java event.data().sessionStatusIdled().ifPresent(idled -> { var session = client.beta().sessions().retrieve(idled.id()); notifyUser(session); }); exchange.sendResponseHeaders(204, -1); ``` ```php PHP if ($event->data->type === 'session.status_idled') { $session = $client->beta->sessions->retrieve($event->data->id); notifyUser($session); } http_response_code(204); ``` ```ruby Ruby if event.data.type == :"session.status_idled" session = client.beta.sessions.retrieve(event.data.id) notify_user(session) end status 204 ``` The top-level `event.id` is unique per event, not per delivery. If you receive the same `event.id` twice, it's a retry and you can discard it. ## Delivery behavior * **Duplicates:** An endpoint can receive the same event more than once, and every attempt delivers the same top-level `event.id` (the same value as the `webhook-id` header). Deduplicate on it. * **Subscription scope:** An event is delivered only to endpoints subscribed to its type at the moment it's emitted. An event emitted while no endpoint is subscribed to its type is never delivered, and subscribing later doesn't backfill it, so subscribe to an event type before you need it. * **Ordering is not guaranteed.** Events aren't delivered in the order they occurred: `session.status_idled` might arrive before `session.outcome_evaluation_ended` even if the outcome was produced first, and a `.deleted` event can arrive before the `.archived` event for the same resource. Drive your state from the resource you fetch, not from the order events arrive in. * **Retries:** For each endpoint and event, Anthropic makes up to three delivery attempts (a response that triggers auto-disable, described later in this section, is never retried) with jittered exponential backoff between 5 and 120 seconds. Every attempt delivers the same `event.id`. After the last attempt fails, the event is dropped: it isn't queued for later delivery and there's no signal that it was lost. Webhooks aren't a durable log, so if you need to observe every transition, reconcile by listing or fetching the resource through the API. * **Timestamps:** The `webhook-timestamp` header is stamped when a delivery attempt is signed and is regenerated on every retry, so retries aren't rejected by the SDK's freshness check. It's the clock for the delivery attempt, not for the event: use the event payload's `created_at` for when the event occurred. * **Auto-disable:** An endpoint is automatically set to `disabled` with a machine-readable `disabled_reason` in three cases: * The endpoint returns a `3xx` response. Redirects are never followed; this disables the endpoint immediately, on the first attempt, with the reason `auto-disabled: endpoint URL returned a redirect (3xx)`. If your endpoint moves, update the URL in Console and re-enable the endpoint. * The endpoint's URL resolves to a non-public IP address when Anthropic connects. This disables the endpoint immediately, with the reason `auto-disabled: endpoint URL resolved to an invalid address`. * Deliveries to the endpoint fail continuously for a sustained period, with the reason `auto-disabled after sustained delivery failures`. The trigger is how long the endpoint has been failing without interruption, not a delivery count. A single `2xx` resets the window, so one flaky event can't disable the endpoint. All three are reversible: re-enable the endpoint in Console after you resolve the issue. Events emitted while the endpoint was disabled aren't replayed. ### Manage agent context --- title: Accessing GitHub url: https://platform.claude.com/docs/en/managed-agents/github description: Connect your agent to GitHub repositories for cloning, reading, and creating pull requests. --- You can mount a GitHub repository to your session sandbox and connect to the GitHub MCP for making pull requests. GitHub repositories are cached, so future sessions that use the same repository start faster. Managed Agents API requests require the `managed-agents-2026-04-01` beta header, except memory store endpoints, which use `agent-memory-2026-07-22` instead. The SDK sets the correct beta header automatically. See [Beta headers](https://platform.claude.com/docs/en/api/beta-headers#endpoint-specific-headers). ## GitHub MCP and session resources First, create an agent that declares the GitHub MCP server. The agent definition holds the server URL but no authentication token: ```bash cURL agent_id=$(curl -fsS https://api.anthropic.com/v1/agents \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ --data @- <beta->agents->create( name: 'Code Reviewer', model: 'claude-opus-5', system: 'You are a code review assistant with access to GitHub.', mcpServers: [ [ 'type' => 'url', 'name' => 'github', 'url' => 'https://api.githubcopilot.com/mcp/', ], ], tools: [ ['type' => 'agent_toolset_20260401'], [ 'type' => 'mcp_toolset', 'mcpServerName' => 'github', ], ], ); ``` ```ruby Ruby agent = client.beta.agents.create( name: "Code Reviewer", model: "claude-opus-5", system_: "You are a code review assistant with access to GitHub.", mcp_servers: [ { type: "url", name: "github", url: "https://api.githubcopilot.com/mcp/" } ], tools: [ {type: "agent_toolset_20260401"}, { type: "mcp_toolset", mcp_server_name: "github" } ] ) ``` Then create a session that mounts the GitHub repository: ```bash cURL session_id=$(curl -fsS https://api.anthropic.com/v1/sessions \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ --data @- <beta->sessions->create( agent: $agent->id, environmentID: $environment->id, resources: [ [ 'type' => 'github_repository', 'url' => 'https://github.com/org/repo', 'mountPath' => '/workspace/repo', 'authorizationToken' => 'ghp_your_github_token', ], ], ); ``` ```ruby Ruby session = client.beta.sessions.create( agent: agent.id, environment_id: environment.id, resources: [ { type: "github_repository", url: "https://github.com/org/repo", mount_path: "/workspace/repo", authorization_token: "ghp_your_github_token" } ] ) ``` The `resources[].authorization_token` authenticates the repository clone operation and is not echoed in API responses. Mounting a repository also loads any skills stored in its root `.claude/skills` directory. Skills are discovered once per session, from the repository state checked out at session start. See [Load skills from a GitHub repository](https://platform.claude.com/docs/en/managed-agents/skills#load-skills-from-a-github-repository). ## Token permissions When providing a GitHub token, use the minimum required permissions: | Action | Required scopes | | ------------------- | --------------------------------- | | Clone private repos | `repo` | | Create PRs | `repo` | | Read issues | `repo` (private) or `public_repo` | | Create issues | `repo` (private) or `public_repo` | Use fine-grained personal access tokens with minimum required permissions. Avoid using tokens with broad access to your GitHub account. ## Multiple repositories Mount multiple repositories by adding entries to the `resources` array: ```bash cURL resources='[ { "type": "github_repository", "url": "https://github.com/org/frontend", "mount_path": "/workspace/frontend", "authorization_token": "ghp_your_github_token" }, { "type": "github_repository", "url": "https://github.com/org/backend", "mount_path": "/workspace/backend", "authorization_token": "ghp_your_github_token" } ]' ``` ```bash CLI RESOURCES_BODY=$(cat <<'EOF' resources: - type: github_repository url: https://github.com/org/frontend mount_path: /workspace/frontend authorization_token: ghp_your_github_token - type: github_repository url: https://github.com/org/backend mount_path: /workspace/backend authorization_token: ghp_your_github_token EOF ) ``` ```python Python resources = [ { "type": "github_repository", "url": "https://github.com/org/frontend", "mount_path": "/workspace/frontend", "authorization_token": "ghp_your_github_token", }, { "type": "github_repository", "url": "https://github.com/org/backend", "mount_path": "/workspace/backend", "authorization_token": "ghp_your_github_token", }, ] ``` ```typescript TypeScript const resources = [ { type: "github_repository", url: "https://github.com/org/frontend", mount_path: "/workspace/frontend", authorization_token: "ghp_your_github_token", }, { type: "github_repository", url: "https://github.com/org/backend", mount_path: "/workspace/backend", authorization_token: "ghp_your_github_token", }, ]; ``` ```csharp C# BetaManagedAgentsGitHubRepositoryResourceParams[] resources = [ new() { Type = "github_repository", Url = "https://github.com/org/frontend", MountPath = "/workspace/frontend", AuthorizationToken = "ghp_your_github_token", }, new() { Type = "github_repository", Url = "https://github.com/org/backend", MountPath = "/workspace/backend", AuthorizationToken = "ghp_your_github_token", }, ]; ``` ```go Go resources := []anthropic.BetaSessionNewParamsResourceUnion{ { OfGitHubRepository: &anthropic.BetaManagedAgentsGitHubRepositoryResourceParams{ Type: anthropic.BetaManagedAgentsGitHubRepositoryResourceParamsTypeGitHubRepository, URL: "https://github.com/org/frontend", MountPath: anthropic.String("/workspace/frontend"), AuthorizationToken: "ghp_your_github_token", }, }, { OfGitHubRepository: &anthropic.BetaManagedAgentsGitHubRepositoryResourceParams{ Type: anthropic.BetaManagedAgentsGitHubRepositoryResourceParamsTypeGitHubRepository, URL: "https://github.com/org/backend", MountPath: anthropic.String("/workspace/backend"), AuthorizationToken: "ghp_your_github_token", }, }, } ``` ```java Java var resources = List.of( BetaManagedAgentsGitHubRepositoryResourceParams.builder() .type(BetaManagedAgentsGitHubRepositoryResourceParams.Type.GITHUB_REPOSITORY) .url("https://github.com/org/frontend") .mountPath("/workspace/frontend") .authorizationToken("ghp_your_github_token") .build(), BetaManagedAgentsGitHubRepositoryResourceParams.builder() .type(BetaManagedAgentsGitHubRepositoryResourceParams.Type.GITHUB_REPOSITORY) .url("https://github.com/org/backend") .mountPath("/workspace/backend") .authorizationToken("ghp_your_github_token") .build()); ``` ```php PHP $resources = [ [ 'type' => 'github_repository', 'url' => 'https://github.com/org/frontend', 'mountPath' => '/workspace/frontend', 'authorizationToken' => 'ghp_your_github_token', ], [ 'type' => 'github_repository', 'url' => 'https://github.com/org/backend', 'mountPath' => '/workspace/backend', 'authorizationToken' => 'ghp_your_github_token', ], ]; ``` ```ruby Ruby resources = [ { type: "github_repository", url: "https://github.com/org/frontend", mount_path: "/workspace/frontend", authorization_token: "ghp_your_github_token" }, { type: "github_repository", url: "https://github.com/org/backend", mount_path: "/workspace/backend", authorization_token: "ghp_your_github_token" } ] ``` ## Managing repositories on a running session After a session is created, you can list its repository resources and rotate their authorization tokens. Each resource has an `id` returned at session creation time (or through `resources.list`) that you use for updates. Repositories are attached for the lifetime of the session; to change which repositories are mounted, create a new session. ```bash cURL # List resources on the session repo_resource_id=$(curl -fsS "https://api.anthropic.com/v1/sessions/$session_id/resources" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" | jq -r '.data[0].id') echo "$repo_resource_id" # "sesrsc_01ABC..." # Rotate the authorization token curl -fsS "https://api.anthropic.com/v1/sessions/$session_id/resources/$repo_resource_id" \ # ... -o /dev/null \ --data @- < entry.type === "github_repository", ); if (!repoResource) { throw new Error("No GitHub repository resource on the session"); } const repoResourceId = repoResource.id; console.log(repoResourceId); // "sesrsc_01ABC..." // Rotate the authorization token await client.beta.sessions.resources.update(repoResourceId, { session_id: session.id, authorization_token: "ghp_your_new_github_token", }); ``` ```csharp C# // List resources on the session var listed = await client.Beta.Sessions.Resources.List(session.ID); var repoResourceId = (await listed.Paginate().FirstAsync()).ID; Console.WriteLine(repoResourceId); // "sesrsc_01ABC..." // Rotate the authorization token await client.Beta.Sessions.Resources.Update(repoResourceId, new() { SessionID = session.ID, AuthorizationToken = "ghp_your_new_github_token", }); ``` ```go Go // List resources on the session listed, err := client.Beta.Sessions.Resources.List(ctx, session.ID, anthropic.BetaSessionResourceListParams{}) if err != nil { panic(err) } repoResourceID := listed.Data[0].ID fmt.Println(repoResourceID) // "sesrsc_01ABC..." // Rotate the authorization token _, err = client.Beta.Sessions.Resources.Update(ctx, repoResourceID, anthropic.BetaSessionResourceUpdateParams{ SessionID: session.ID, AuthorizationToken: "ghp_your_new_github_token", }) if err != nil { panic(err) } ``` ```java Java // List resources on the session var listed = client.beta().sessions().resources().list(session.id()); var repoResourceId = listed.data().getFirst().asGitHubRepository().id(); IO.println(repoResourceId); // "sesrsc_01ABC..." // Rotate the authorization token client.beta().sessions().resources().update(repoResourceId, ResourceUpdateParams.builder() .sessionId(session.id()) .authorizationToken("ghp_your_new_github_token") .build()); ``` ```php PHP // List resources on the session $listed = $client->beta->sessions->resources->list($session->id); $repoResourceId = $listed->data[0]->id; echo $repoResourceId, PHP_EOL; // "sesrsc_01ABC..." // Rotate the authorization token $client->beta->sessions->resources->update( $repoResourceId, sessionID: $session->id, authorizationToken: 'ghp_your_new_github_token', ); ``` ```ruby Ruby # List resources on the session listed = client.beta.sessions.resources.list(session.id) repo_resource_id = listed.data.first.id puts repo_resource_id # "sesrsc_01ABC..." # Rotate the authorization token client.beta.sessions.resources.update( repo_resource_id, session_id: session.id, authorization_token: "ghp_your_new_github_token" ) ``` ## Creating pull requests With the GitHub MCP server, the agent can create branches, commit changes, and push them: ```bash cURL curl -fsS "https://api.anthropic.com/v1/sessions/$session_id/events" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -o /dev/null \ --data @- < /dev/null <<'EOF' events: - type: user.message content: - type: text text: Fix the type error in src/utils.ts, commit it to a new branch, and push it. EOF ``` ```python Python client.beta.sessions.events.send( session.id, events=[ { "type": "user.message", "content": [ { "type": "text", "text": "Fix the type error in src/utils.ts, commit it to a new branch, and push it.", }, ], }, ], ) ``` ```typescript TypeScript await client.beta.sessions.events.send(session.id, { events: [ { type: "user.message", content: [ { type: "text", text: "Fix the type error in src/utils.ts, commit it to a new branch, and push it.", }, ], }, ], }); ``` ```csharp C# await client.Beta.Sessions.Events.Send(session.ID, new() { Events = [ new BetaManagedAgentsUserMessageEventParams { Type = "user.message", Content = [ new BetaManagedAgentsTextBlock { Type = "text", Text = "Fix the type error in src/utils.ts, commit it to a new branch, and push it.", }, ], }, ], }); ``` ```go Go _, err = client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{ Events: []anthropic.BetaManagedAgentsEventParamsUnion{ { OfUserMessage: &anthropic.BetaManagedAgentsUserMessageEventParams{ Type: anthropic.BetaManagedAgentsUserMessageEventParamsTypeUserMessage, Content: []anthropic.BetaManagedAgentsUserMessageEventParamsContentUnion{ { OfText: &anthropic.BetaManagedAgentsTextBlockParam{ Type: anthropic.BetaManagedAgentsTextBlockTypeText, Text: "Fix the type error in src/utils.ts, commit it to a new branch, and push it.", }, }, }, }, }, }, }) if err != nil { panic(err) } ``` ```java Java client.beta().sessions().events().send(session.id(), EventSendParams.builder() .addEvent(BetaManagedAgentsUserMessageEventParams.builder() .type(BetaManagedAgentsUserMessageEventParams.Type.USER_MESSAGE) .addContent(BetaManagedAgentsTextBlock.builder() .type(BetaManagedAgentsTextBlock.Type.TEXT) .text("Fix the type error in src/utils.ts, commit it to a new branch, and push it.") .build()) .build()) .build()); ``` ```php PHP $client->beta->sessions->events->send( $session->id, events: [ [ 'type' => 'user.message', 'content' => [ [ 'type' => 'text', 'text' => 'Fix the type error in src/utils.ts, commit it to a new branch, and push it.', ], ], ], ], ); ``` ```ruby Ruby client.beta.sessions.events.send_( session.id, events: [ { type: "user.message", content: [ { type: "text", text: "Fix the type error in src/utils.ts, commit it to a new branch, and push it." } ] } ] ) ``` ## Next steps Stream events and steer the agent while it opens the pull request Connect more MCP servers to give the agent additional tools Mount files in the sandbox alongside your repositories --- title: Adding files url: https://platform.claude.com/docs/en/managed-agents/files description: Upload files and mount them in your sandbox for reading and processing. --- You can provide files to your agent by uploading them through the Files API and mounting them in the session's sandbox. Managed Agents API requests require the `managed-agents-2026-04-01` beta header, except memory store endpoints, which use `agent-memory-2026-07-22` instead. The SDK sets the correct beta header automatically. See [Beta headers](https://platform.claude.com/docs/en/api/beta-headers#endpoint-specific-headers). ## Uploading files First, upload a file using the [Files API](https://platform.claude.com/docs/en/build-with-claude/files): ```bash cURL file=$(curl --fail-with-body -sS "${auth[@]}" \ "${base_url}/files" \ -F file=@data.csv) file_id=$(jq -er '.id' <<<"${file}") printf 'File ID: %s\n' "${file_id}" ``` ```bash CLI FILE_ID=$(ant beta:files upload \ --file data.csv \ --transform id --raw-output) ``` ```python Python file = client.beta.files.upload(file=Path("data.csv")) print(f"File ID: {file.id}") ``` ```typescript TypeScript const file = await client.beta.files.upload({ file: await toFile(readFile("data.csv"), "data.csv", { type: "text/csv" }), }); console.log(`File ID: ${file.id}`); ``` ```csharp C# await using var stream = File.OpenRead(csvPath); var file = await client.Beta.Files.Upload(new() { File = stream }); Console.WriteLine($"File ID: {file.ID}"); ``` ```go Go csvFile, err := os.Open("data.csv") if err != nil { panic(err) } defer csvFile.Close() file, err := client.Beta.Files.Upload(ctx, anthropic.BetaFileUploadParams{ File: csvFile, }) if err != nil { panic(err) } fmt.Printf("File ID: %s\n", file.ID) ``` ```java Java var file = client.beta().files().upload( FileUploadParams.builder().file(dataCsv).build() ); IO.println("File ID: " + file.id()); ``` ```php PHP $file = $client->beta->files->upload( FileParam::fromResource(fopen($csvPath, 'r'), filename: 'data.csv', contentType: 'text/csv'), ); echo "File ID: {$file->id}\n"; ``` ```ruby Ruby file = client.beta.files.upload(file: Pathname(csv_path)) puts "File ID: #{file.id}" ``` ## Mounting files in a session Mount uploaded files into the sandbox by adding them to the `resources` array when creating a session: The `mount_path` is optional, but make sure the uploaded file has a descriptive name so the agent can identify it. ```bash cURL session=$( jq -n \ --arg agent_id "${agent_id}" \ --arg environment_id "${environment_id}" \ --arg file_id "${file_id}" \ '{ agent: $agent_id, environment_id: $environment_id, resources: [ { type: "file", file_id: $file_id, mount_path: "/data.csv" } ] }' | curl --fail-with-body -sS "${auth[@]}" "${base_url}/sessions" --json @- ) session_id=$(jq -er '.id' <<<"${session}") ``` ```bash CLI SESSION_ID=$(ant beta:sessions create \ --agent "$AGENT_ID" \ --environment-id "$ENVIRONMENT_ID" \ --transform id --raw-output <beta->sessions->create( agent: $agent->id, environmentID: $environment->id, resources: [ BetaManagedAgentsFileResourceParams::with( type: 'file', fileID: $file->id, mountPath: '/data.csv', ), ], ); ``` ```ruby Ruby session = client.beta.sessions.create( agent: agent.id, environment_id: environment.id, resources: [ { type: "file", file_id: file.id, mount_path: "/data.csv" } ] ) ``` With the preceding `mount_path`, the agent reads the file at `/mnt/session/uploads/data.csv` (see [File paths](https://platform.claude.com/docs/en/managed-agents/files#file-paths)). A new `file_id` is created that references the instance of the file in the session. These copies do not count against your [storage limits](https://platform.claude.com/docs/en/build-with-claude/files). ## Multiple files Mount multiple files by adding entries to the `resources` array: ```json cURL "resources": [ { "type": "file", "file_id": "file_abc123", "mount_path": "/data.csv" }, { "type": "file", "file_id": "file_def456", "mount_path": "/config.json" }, { "type": "file", "file_id": "file_ghi789", "mount_path": "/src/main.py" } ] ``` ```yaml CLI resources: - type: file file_id: file_abc123 mount_path: /data.csv - type: file file_id: file_def456 mount_path: /config.json - type: file file_id: file_ghi789 mount_path: /src/main.py ``` ```python Python resources = [ {"type": "file", "file_id": "file_abc123", "mount_path": "/data.csv"}, {"type": "file", "file_id": "file_def456", "mount_path": "/config.json"}, {"type": "file", "file_id": "file_ghi789", "mount_path": "/src/main.py"}, ] ``` ```typescript TypeScript resources: [ { type: "file", file_id: "file_abc123", mount_path: "/data.csv" }, { type: "file", file_id: "file_def456", mount_path: "/config.json" }, { type: "file", file_id: "file_ghi789", mount_path: "/src/main.py" } ] ``` ```csharp C# using Anthropic.Models.Beta.Sessions; var resources = new[] { new BetaManagedAgentsFileResourceParams { Type = BetaManagedAgentsFileResourceParamsType.File, FileID = "file_abc123", MountPath = "/data.csv" }, new BetaManagedAgentsFileResourceParams { Type = BetaManagedAgentsFileResourceParamsType.File, FileID = "file_def456", MountPath = "/config.json" }, new BetaManagedAgentsFileResourceParams { Type = BetaManagedAgentsFileResourceParamsType.File, FileID = "file_ghi789", MountPath = "/src/main.py" }, }; ``` ```go Go resources := []anthropic.BetaSessionNewParamsResourceUnion{ {OfFile: &anthropic.BetaManagedAgentsFileResourceParams{Type: "file", FileID: "file_abc123", MountPath: anthropic.String("/data.csv")}}, {OfFile: &anthropic.BetaManagedAgentsFileResourceParams{Type: "file", FileID: "file_def456", MountPath: anthropic.String("/config.json")}}, {OfFile: &anthropic.BetaManagedAgentsFileResourceParams{Type: "file", FileID: "file_ghi789", MountPath: anthropic.String("/src/main.py")}}, } ``` ```java Java import com.anthropic.models.beta.sessions.*; import java.util.List; var resources = List.of( BetaManagedAgentsFileResourceParams.builder() .type(BetaManagedAgentsFileResourceParams.Type.FILE).fileId("file_abc123").mountPath("/data.csv").build(), BetaManagedAgentsFileResourceParams.builder() .type(BetaManagedAgentsFileResourceParams.Type.FILE).fileId("file_def456").mountPath("/config.json").build(), BetaManagedAgentsFileResourceParams.builder() .type(BetaManagedAgentsFileResourceParams.Type.FILE).fileId("file_ghi789").mountPath("/src/main.py").build() ); ``` ```php PHP $resources = [ ['type' => 'file', 'file_id' => 'file_abc123', 'mount_path' => '/data.csv'], ['type' => 'file', 'file_id' => 'file_def456', 'mount_path' => '/config.json'], ['type' => 'file', 'file_id' => 'file_ghi789', 'mount_path' => '/src/main.py'], ]; ``` ```ruby Ruby resources = [ {type: "file", file_id: "file_abc123", mount_path: "/data.csv"}, {type: "file", file_id: "file_def456", mount_path: "/config.json"}, {type: "file", file_id: "file_ghi789", mount_path: "/src/main.py"} ] ``` A maximum of 500 files is supported per session. ## Managing files on a running session You can add or remove files from a session after creation using the session resources API. Each resource has an `id` returned when it is added (or listed), which you use for deletes. ```bash cURL resource=$( jq -n --arg file_id "${file_id}" '{type: "file", file_id: $file_id}' \ | curl --fail-with-body -sS "${auth[@]}" \ "${base_url}/sessions/${session_id}/resources" --json @- ) resource_id=$(jq -er '.id' <<<"${resource}") printf '%s\n' "${resource_id}" # "sesrsc_01ABC..." ``` ```bash CLI RESOURCE_ID=$(ant beta:sessions:resources add \ --session-id "$SESSION_ID" \ --type file \ --file-id "$FILE_ID" \ --transform id --raw-output) ``` ```python Python resource = client.beta.sessions.resources.add( session.id, type="file", file_id=file.id, ) print(resource.id) # "sesrsc_01ABC..." ``` ```typescript TypeScript const resource = await client.beta.sessions.resources.add(session.id, { type: "file", file_id: file.id, }); if (resource.type !== "file") { throw new Error(`Unexpected resource type: ${resource.type}`); } console.log(resource.id); // "sesrsc_01ABC..." ``` ```csharp C# var resource = await client.Beta.Sessions.Resources.Add(session.ID, new() { Type = "file", FileID = file.ID, }); Console.WriteLine(resource.ID); // "sesrsc_01ABC..." ``` ```go Go resource, err := client.Beta.Sessions.Resources.Add(ctx, session.ID, anthropic.BetaSessionResourceAddParams{ BetaManagedAgentsFileResourceParams: anthropic.BetaManagedAgentsFileResourceParams{ Type: anthropic.BetaManagedAgentsFileResourceParamsTypeFile, FileID: file.ID, }, }) if err != nil { panic(err) } fmt.Println(resource.ID) // "sesrsc_01ABC..." ``` ```java Java var resource = client.beta().sessions().resources().add( session.id(), ResourceAddParams.builder() .betaManagedAgentsFileResourceParams( BetaManagedAgentsFileResourceParams.builder() .type(BetaManagedAgentsFileResourceParams.Type.FILE) .fileId(file.id()) .build() ) .build() ); IO.println(resource.id()); // "sesrsc_01ABC..." ``` ```php PHP $resource = $client->beta->sessions->resources->add( $session->id, type: 'file', fileID: $file->id, ); echo "{$resource->id}\n"; // "sesrsc_01ABC..." ``` ```ruby Ruby resource = client.beta.sessions.resources.add( session.id, type: "file", file_id: file.id ) puts resource.id # "sesrsc_01ABC..." ``` List all resources on a session with `resources.list`. To remove a file, call `resources.delete` with the resource ID: ```bash cURL curl --fail-with-body -sS "${auth[@]}" \ "${base_url}/sessions/${session_id}/resources" \ | jq -r '.data[] | "\(.id) \(.type)"' curl --fail-with-body -sS "${auth[@]}" -X DELETE \ "${base_url}/sessions/${session_id}/resources/${resource_id}" >/dev/null ``` ```bash CLI ant beta:sessions:resources list --session-id "$SESSION_ID" ant beta:sessions:resources delete \ --session-id "$SESSION_ID" \ --resource-id "$RESOURCE_ID" ``` ```python Python listed = client.beta.sessions.resources.list(session.id) for entry in listed.data: print(entry.id, entry.type) client.beta.sessions.resources.delete(resource.id, session_id=session.id) ``` ```typescript TypeScript const listed = await client.beta.sessions.resources.list(session.id); for (const entry of listed.data) { if (entry.type !== "memory_store") { console.log(entry.id, entry.type); } } await client.beta.sessions.resources.delete(resource.id, { session_id: session.id, }); ``` ```csharp C# var listed = await client.Beta.Sessions.Resources.List(session.ID); await foreach (var entry in listed.Paginate()) { var type = entry.Match(repo => repo.Type, fileRes => fileRes.Type, memoryStore => memoryStore.Type); Console.WriteLine($"{entry.ID} {type}"); } await client.Beta.Sessions.Resources.Delete(resource.ID, new() { SessionID = session.ID }); ``` ```go Go listed, err := client.Beta.Sessions.Resources.List(ctx, session.ID, anthropic.BetaSessionResourceListParams{}) if err != nil { panic(err) } for _, entry := range listed.Data { fmt.Println(entry.ID, entry.Type) } if _, err := client.Beta.Sessions.Resources.Delete(ctx, resource.ID, anthropic.BetaSessionResourceDeleteParams{ SessionID: session.ID, }); err != nil { panic(err) } ``` ```java Java var listed = client.beta().sessions().resources().list(session.id()); for (var entry : listed.data()) { if (entry.isFile()) { var fileResource = entry.asFile(); IO.println(fileResource.id() + " " + fileResource.type()); } else if (entry.isGitHubRepository()) { var repoResource = entry.asGitHubRepository(); IO.println(repoResource.id() + " " + repoResource.type()); } } client.beta().sessions().resources().delete( resource.id(), ResourceDeleteParams.builder().sessionId(session.id()).build() ); ``` ```php PHP $listed = $client->beta->sessions->resources->list($session->id); foreach ($listed->data as $entry) { echo "{$entry->id} {$entry->type}\n"; } $client->beta->sessions->resources->delete($resource->id, sessionID: $session->id); ``` ```ruby Ruby listed = client.beta.sessions.resources.list(session.id) listed.data.each { puts "#{it.id} #{it.type}" } client.beta.sessions.resources.delete(resource.id, session_id: session.id) ``` ## Listing and downloading session files Use the [Files API](https://platform.claude.com/docs/en/build-with-claude/files) to list files scoped to a session and download them. ```bash cURL # List files associated with a session curl -fsSL "https://api.anthropic.com/v1/files?scope_id=sesn_abc123" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" # Download a file curl -fsSL "https://api.anthropic.com/v1/files/$FILE_ID/content" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -o output.txt ``` ```bash CLI # List files associated with a session ant beta:files list --scope-id sesn_abc123 \ --beta managed-agents-2026-04-01 # Download a file ant beta:files download --file-id "$FILE_ID" --output output.txt ``` ```python Python # List files associated with a session files = client.beta.files.list( scope_id="sesn_abc123", betas=["managed-agents-2026-04-01"], ) for file in files: print(file.id, file.filename) # Download a file content = client.beta.files.download(files.data[0].id) content.write_to_file("output.txt") ``` ```typescript TypeScript // List files associated with a session const files = await client.beta.files.list({ scope_id: "sesn_abc123", betas: ["managed-agents-2026-04-01"] }); for (const file of files.data) { console.log(file.id, file.filename); } // Download a file const content = await client.beta.files.download(files.data[0].id); await content.writeToFile("output.txt"); ``` ```csharp C# // List files associated with a session var files = await client.Beta.Files.List(new FileListParams { ScopeID = "sesn_abc123", Betas = ["managed-agents-2026-04-01"], }); // Download a file byte[] content = await client.Beta.Files.Download(files.Data[0].ID); await File.WriteAllBytesAsync("output.txt", content); ``` ```go Go // List files associated with a session files, err := client.Beta.Files.List(ctx, anthropic.BetaFileListParams{ ScopeID: anthropic.String("sesn_abc123"), Betas: []anthropic.AnthropicBeta{"managed-agents-2026-04-01"}, }) if err != nil { panic(err) } // Download a file resp, err := client.Beta.Files.Download(ctx, files.Data[0].ID, anthropic.BetaFileDownloadParams{}) if err != nil { panic(err) } defer resp.Body.Close() out, err := os.Create("output.txt") if err != nil { panic(err) } defer out.Close() if _, err := io.Copy(out, resp.Body); err != nil { panic(err) } ``` ```java Java // List files associated with a session var files = client.beta().files().list(FileListParams.builder() .scopeId("sesn_abc123") .addBeta(AnthropicBeta.of("managed-agents-2026-04-01")) .build()); // Download a file try (HttpResponse response = client.beta().files().download(files.data().get(0).id())) { try (InputStream body = response.body()) { Files.copy(body, Path.of("output.txt"), StandardCopyOption.REPLACE_EXISTING); } } ``` ```php PHP // List files associated with a session $files = $client->beta->files->list( scopeID: 'sesn_abc123', betas: ['managed-agents-2026-04-01'], ); // Download a file $content = $client->beta->files->download($files->data[0]->id); file_put_contents('output.txt', $content); ``` ```ruby Ruby # List files associated with a session files = client.beta.files.list( scope_id: "sesn_abc123", betas: ["managed-agents-2026-04-01"] ) # Download a file content = client.beta.files.download(files.data[0].id) File.binwrite("output.txt", content.read) ``` ## Supported file types The agent can work with any file type, including: * Source code (`.py`, `.js`, `.ts`, `.go`, `.rs`, and others) * Data files (`.csv`, `.json`, `.xml`, `.yaml`) * Documents (`.txt`, `.md`) * Archives (`.zip`, `.tar.gz`) - the agent can extract these using bash * Binary files - the agent can process these with appropriate tools ## File paths Files mounted in the sandbox are read-only copies. The agent can read them but cannot modify the original uploaded file. To work with modified versions, the agent writes to new paths within the sandbox. * The path you specify is rooted under the session's uploads directory: a `mount_path` of `/data.csv` places the file at `/mnt/session/uploads/data.csv` in the sandbox * If you omit `mount_path`, the file is placed at `/mnt/session/uploads/` * Parent directories are created automatically * Paths should be absolute (starting with `/`) ### Manage agent context > Build persistent memory --- title: Dreams url: https://platform.claude.com/docs/en/managed-agents/dreams description: Let Claude reflect on past sessions to curate an agent's memory and surface new insights. --- Dreaming is a research preview feature. [Request access](https://claude.com/form/claude-managed-agents) to try it. Agents write to their [memory stores](https://platform.claude.com/docs/en/managed-agents/memory) as they work, but these writes are local and incremental: over many sessions a memory store accumulates duplicates, contradictions, and stale entries. **Dreams** let Claude clean that up. A dream reads an existing memory store alongside past session transcripts, then produces a new, reorganized memory store: duplicates merged, stale or contradicted entries replaced with the latest value, and new insights surfaced. The input store is never modified, so you can review the output and discard it if you don't like the result. Dream endpoints are gated by the `dreaming-2026-04-21` beta header; the `managed-agents-2026-04-01` header on its own doesn't grant access to dreams. The dream-endpoint examples on this page send both headers; session and memory-store calls need only `managed-agents-2026-04-01`. The SDK sets these automatically. ## How it works A **dream** is an asynchronous job that takes: * a pre-existing **memory store:** the store Claude verifies, deduplicates, and reorganizes, and * 1 to 100 **sessions:** past transcripts Claude mines for patterns and insights to fold into the output. The dream produces another **output memory store**, separate from the input. The output store ID appears in the dream's `outputs[]` shortly after the dream starts `running`, once the workflow has cloned the input store; a `running` dream can briefly report an empty `outputs[]`. ## Create a dream ```bash cURL dream=$(curl -s https://api.anthropic.com/v1/dreams \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01,dreaming-2026-04-21" \ -H "content-type: application/json" \ --data @- <beta->dreams->create( inputs: [ ['type' => 'memory_store', 'memory_store_id' => $storeId], ['type' => 'sessions', 'session_ids' => [$sessionA, $sessionB]], ], model: 'claude-opus-4-8', instructions: 'Focus on coding-style preferences; ignore one-off debugging notes.', ); echo "{$dream->id}\n"; // drm_01... ``` ```ruby Ruby dream = client.beta.dreams.create( inputs: [ {type: "memory_store", memory_store_id: store_id}, {type: "sessions", session_ids: [session_a, session_b]} ], model: "claude-opus-4-8", instructions: "Focus on coding-style preferences; ignore one-off debugging notes." ) puts dream.id # drm_01... ``` Dreaming inputs include the pre-existing memory store and an array of sessions. The selected model runs the dreaming pipeline; during the research preview `claude-opus-5`, `claude-fable-5`, `claude-opus-4-8`, `claude-opus-4-7`, `claude-sonnet-5`, and `claude-sonnet-4-6` are supported. You can optionally pass `instructions` to steer the dreaming process; see [Steer with instructions](https://platform.claude.com/docs/en/managed-agents/dreams#steer-with-instructions). The response is the full `dream` resource with `status: "pending"`: ```json { "type": "dream", "id": "drm_01AbCDefGhIjKlMnOpQrStUv", "status": "pending", "inputs": [ { "type": "memory_store", "memory_store_id": "memstore_01Hx..." }, { "type": "sessions", "session_ids": ["sesn_01...", "sesn_02..."] } ], "outputs": [], "model": { "id": "claude-opus-4-8" }, "instructions": "Focus on coding-style preferences; ignore one-off debugging notes.", "session_id": null, "created_at": "2026-04-29T17:04:10Z", "ended_at": null, "archived_at": null, "usage": { "input_tokens": 0, "output_tokens": 0, "cache_read_input_tokens": 0, "cache_creation_input_tokens": 0 }, "error": null } ``` If you only have session transcripts and no existing store, [create an empty memory store](https://platform.claude.com/docs/en/managed-agents/memory#create-a-memory-store) first and pass it as the `memory_store` input. ### Steer with instructions The optional `instructions` field steers what the dreaming pipeline synthesizes. It is applied throughout the pipeline: what to read closely, what to merge or drop, and how to structure the output store. Use `instructions` for high-level synthesis guidance such as focus areas ("focus on coding-style preferences"), content to preserve unchanged, or output conventions you want applied across the store. The pipeline is a synthesis pass over the inputs, not an editor applied to the text of the store, so imperative directives that target specific lines ("change sentence X to Y", "fix the count in section Z") generally produce no change. To make targeted edits to individual memories, use the [Memory Stores API](https://platform.claude.com/docs/en/managed-agents/memory#view-and-edit-memories) on the output store directly. ## Track progress Dreams run asynchronously and typically take minutes to a few hours, driven by the number of input transcripts. Poll the dream by ID to check status: ```bash cURL while true; do dream=$(curl -s "https://api.anthropic.com/v1/dreams/$dream_id" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01,dreaming-2026-04-21") status=$(jq -r '.status' <<< "$dream") echo "status=$status input_tokens=$(jq -r '.usage.input_tokens' <<< "$dream")" [[ "$status" == "pending" || "$status" == "running" ]] || break sleep 10 done ``` ```bash CLI ant beta:dreams retrieve --dream-id "$dream_id" ``` ```python Python while dream.status in ("pending", "running"): time.sleep(10) dream = client.beta.dreams.retrieve(dream.id) print(f"status={dream.status} input_tokens={dream.usage.input_tokens}") ``` ```typescript TypeScript while (dream.status === "pending" || dream.status === "running") { await sleep(10_000); dream = await client.beta.dreams.retrieve(dream.id); console.log(`status=${dream.status} input_tokens=${dream.usage.input_tokens}`); } ``` ```csharp C# while (dream.Status.Value() is BetaDreamStatus.Pending or BetaDreamStatus.Running) { await Task.Delay(TimeSpan.FromSeconds(10)); dream = await client.Beta.Dreams.Retrieve(dream.ID); Console.WriteLine($"status={dream.Status.Raw()} input_tokens={dream.Usage.InputTokens}"); } ``` ```go Go for dream.Status == anthropic.BetaDreamStatusPending || dream.Status == anthropic.BetaDreamStatusRunning { time.Sleep(10 * time.Second) dream, err = client.Beta.Dreams.Get(ctx, dream.ID, anthropic.BetaDreamGetParams{}) if err != nil { panic(err) } fmt.Printf("status=%s input_tokens=%d\n", dream.Status, dream.Usage.InputTokens) } ``` ```java Java while (dream.status().equals(BetaDreamStatus.PENDING) || dream.status().equals(BetaDreamStatus.RUNNING)) { Thread.sleep(10_000); dream = client.beta().dreams().retrieve(dream.id()); IO.println("status=" + dream.status() + " input_tokens=" + dream.usage().inputTokens()); } ``` ```php PHP while (in_array($dream->status, [BetaDreamStatus::PENDING->value, BetaDreamStatus::RUNNING->value], true)) { sleep(10); $dream = $client->beta->dreams->retrieve($dream->id); echo "status={$dream->status} input_tokens={$dream->usage->inputTokens}\n"; } ``` ```ruby Ruby while %i[pending running].include?(dream.status) sleep 10 dream = client.beta.dreams.retrieve(dream.id) puts "status=#{dream.status} input_tokens=#{dream.usage.input_tokens}" end ``` ### Lifecycle | `status` | Meaning | | ----------- | ----------------------------------------------------------------------------------------------------------------- | | `pending` | Dream successfully created and queued. | | `running` | The pipeline is processing. `usage` updates as work progresses. | | `completed` | Finished successfully. The `outputs[]` value is the new memory store. | | `failed` | Dreaming run ended with an error. The output memory store is left as-is with whatever was written before failure. | | `canceled` | Dreaming run canceled. The output memory store is left as-is. | ### Watch the pipeline run Once a dream is `running`, its `session_id` field points at the underlying [session](https://platform.claude.com/docs/en/managed-agents/sessions) running the pipeline. You can stream that session's [events](https://platform.claude.com/docs/en/managed-agents/events-and-streaming) to observe what the dream is reading and writing in real time. The session is archived (not deleted) when the dream reaches a terminal state, so the transcript remains available afterward. ## Use the output When `status` reaches `completed`, the `memory_store` entry in `outputs[]` references a fully populated store. It's an ordinary memory store in your workspace. Review it with the [Memory Stores API](https://platform.claude.com/docs/en/managed-agents/memory#view-and-edit-memories) or in the Console, then either: * **Leverage it:** attach it to future sessions as a `memory_store` resource in place of (or alongside) the input memory store, or * **Discard it:** [delete the memory store](https://platform.claude.com/docs/en/api/beta/memory_stores/delete) or [archive the memory store](https://platform.claude.com/docs/en/api/beta/memory_stores/archive). ```bash cURL # After the dream ends, the memory_store output holds the rebuilt store output_store_id=$(jq -r 'first(.outputs[] | select(.type == "memory_store")).memory_store_id' <<< "$dream") curl -s https://api.anthropic.com/v1/sessions \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ --data @- < entry.type === "memory_store"); const outputStoreId = output!.memory_store_id; await client.beta.sessions.create({ agent: agentId, environment_id: environmentId, resources: [ { type: "memory_store", memory_store_id: outputStoreId }, ], }); ``` ```csharp C# var output = dream.Outputs.FirstOrDefault(entry => entry.Type == "memory_store"); if (output is { MemoryStoreID: var outputStoreID }) { await client.Beta.Sessions.Create(new() { Agent = agentID, EnvironmentID = environmentID, Resources = [ new BetaManagedAgentsMemoryStoreResourceParam { Type = BetaManagedAgentsMemoryStoreResourceParamType.MemoryStore, MemoryStoreID = outputStoreID, }, ], }); } ``` ```go Go for _, output := range dream.Outputs { if output.Type != "memory_store" { continue } outputStoreID := output.MemoryStoreID session, err := client.Beta.Sessions.New(ctx, anthropic.BetaSessionNewParams{ Agent: anthropic.BetaSessionNewParamsAgentUnion{ OfString: anthropic.String(agentID), }, EnvironmentID: environmentID, Resources: []anthropic.BetaSessionNewParamsResourceUnion{{ OfMemoryStore: &anthropic.BetaManagedAgentsMemoryStoreResourceParam{ MemoryStoreID: outputStoreID, }, }}, }) if err != nil { panic(err) } fmt.Println(session.ID) break } ``` ```java Java var output = dream.outputs().stream() .filter(entry -> entry.type().equals(BetaDreamOutput.Type.MEMORY_STORE)) .findFirst(); if (output.isPresent()) { var outputStoreId = output.get().memoryStoreId(); var session = client.beta().sessions().create( SessionCreateParams.builder() .agent(agentId) .environmentId(environmentId) .addMemoryStoreResource(outputStoreId) .build() ); } ``` ```php PHP $matches = array_filter($dream->outputs, fn($output) => $output->type === 'memory_store'); $output = $matches ? reset($matches) : null; if ($output !== null) { $session = $client->beta->sessions->create( agent: $agentId, environmentID: $environmentId, resources: [ ['type' => 'memory_store', 'memory_store_id' => $output->memoryStoreID], ], ); } ``` ```ruby Ruby output = dream.outputs.find { it.type == :memory_store } if output client.beta.sessions.create( agent: agent_id, environment_id: environment_id, resources: [ {type: "memory_store", memory_store_id: output.memory_store_id} ] ) end ``` The dream itself never deletes or modifies its inputs. On `failed` or `canceled` the output store persists with partial contents so you can inspect what was produced before stopping; clean it up through the Memory Stores API if you don't need it. While a dream is `pending` or `running`, the 400 guard applies to archiving the dream itself, not its stores. Archiving or deleting an *input* memory store mid-run (or deleting an input session) will cause the dream to fail with `input_memory_store_unavailable` or `input_session_unavailable`. ## Cancel a dream Cancel moves a `pending` or `running` dream to `canceled` immediately. Canceling an already-`canceled` dream is an idempotent no-op; canceling a `completed` or `failed` dream returns 400. After cancellation, the dream's `usage` fields might continue to update for a few seconds while in-flight work winds down. Poll the dream until `usage` stabilizes if you need the final count. ```bash cURL curl -s -X POST "https://api.anthropic.com/v1/dreams/$dream_id/cancel" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01,dreaming-2026-04-21" ``` ```bash CLI ant beta:dreams cancel --dream-id "$dream_id" ``` ```python Python client.beta.dreams.cancel(dream.id) ``` ```typescript TypeScript await client.beta.dreams.cancel(dream.id); ``` ```csharp C# await client.Beta.Dreams.Cancel(dream.ID); ``` ```go Go dream, err = client.Beta.Dreams.Cancel(ctx, dream.ID, anthropic.BetaDreamCancelParams{}) if err != nil { panic(err) } ``` ```java Java client.beta().dreams().cancel(dream.id()); ``` ```php PHP $client->beta->dreams->cancel($dream->id); ``` ```ruby Ruby client.beta.dreams.cancel(dream.id) ``` ## Archive a dream Archive sets `archived_at` on a dream that has reached a terminal state (`completed`, `failed`, or `canceled`); `status` is left unchanged. Archived dreams are excluded from default list responses but remain readable by ID. Archiving an already-archived dream is an idempotent no-op. Archiving a `pending` or `running` dream returns 400; cancel it first. There is no unarchive. ```bash cURL curl -s -X POST "https://api.anthropic.com/v1/dreams/$dream_id/archive" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01,dreaming-2026-04-21" ``` ```bash CLI ant beta:dreams archive --dream-id "$dream_id" ``` ```python Python client.beta.dreams.archive(dream.id) ``` ```typescript TypeScript await client.beta.dreams.archive(dream.id); ``` ```csharp C# await client.Beta.Dreams.Archive(dream.ID); ``` ```go Go dream, err = client.Beta.Dreams.Archive(ctx, dream.ID, anthropic.BetaDreamArchiveParams{}) if err != nil { panic(err) } ``` ```java Java client.beta().dreams().archive(dream.id()); ``` ```php PHP $client->beta->dreams->archive($dream->id); ``` ```ruby Ruby client.beta.dreams.archive(dream.id) ``` Archiving a dream does not touch its output memory store; manage that separately through the [Memory Stores API](https://platform.claude.com/docs/en/managed-agents/memory#view-and-edit-memories). ## List dreams Returns all non-archived dreams in the workspace, newest first. Use `limit` (default 20, max 100) and the `page` cursor to paginate. Pass `include_archived=true` to include archived dreams. ```bash cURL curl -s "https://api.anthropic.com/v1/dreams?limit=20" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01,dreaming-2026-04-21" ``` ```bash CLI ant beta:dreams list --limit 20 ``` ```python Python for listed_dream in client.beta.dreams.list(limit=20): print(listed_dream.id, listed_dream.status) ``` ```typescript TypeScript for await (const listedDream of client.beta.dreams.list({ limit: 20 })) { console.log(listedDream.id, listedDream.status); } ``` ```csharp C# var page = await client.Beta.Dreams.List(new() { Limit = 20 }); await foreach (var listed in page.Paginate()) { Console.WriteLine($"{listed.ID} {listed.Status.Raw()}"); } ``` ```go Go dreams := client.Beta.Dreams.ListAutoPaging(ctx, anthropic.BetaDreamListParams{ Limit: anthropic.Int(20), }) for dreams.Next() { listed := dreams.Current() fmt.Println(listed.ID, listed.Status) } if err := dreams.Err(); err != nil { panic(err) } ``` ```java Java for (var listedDream : client.beta().dreams().list( DreamListParams.builder().limit(20).build() ).autoPager()) { IO.println(listedDream.id() + " " + listedDream.status()); } ``` ```php PHP foreach ($client->beta->dreams->list(limit: 20)->pagingEachItem() as $dream) { echo "{$dream->id} {$dream->status}\n"; } ``` ```ruby Ruby client.beta.dreams.list(limit: 20).auto_paging_each do puts "#{it.id} #{it.status}" end ``` ## Errors A non-exhaustive list of possible dreaming errors follows. | `error.type` | When | | --------------------------------- | ----------------------------------------------------------------------------------------------- | | `timeout` | The pipeline exceeded its runtime budget. | | `internal_error` | Unclassified pipeline failure. | | `memory_store_org_limit_exceeded` | Your organization hit its memory-store cap while the pipeline was provisioning working storage. | | `input_memory_store_too_large` | The input memory store exceeds the pipeline's size limit. | | `input_memory_store_unavailable` | The input memory store was archived or deleted after the dream was created. | | `input_session_unavailable` | An input session was deleted after the dream was created. | ## Billing Dreams are billed at standard API token rates for the model you select; `usage` on the resource reports the exact totals. Cost scales roughly linearly with the number and length of input sessions. Start with a small batch of sessions and scale up once you're satisfied with the curation quality. ## Limits | Limit | Value | | --------------------- | --------------------------------------------------------------------------------------------------------------- | | Sessions per dream | 100 | | `instructions` length | 4,096 characters | | Supported models | `claude-opus-5`, `claude-fable-5`, `claude-opus-4-8`, `claude-opus-4-7`, `claude-sonnet-5`, `claude-sonnet-4-6` | Default rate limits apply to dream creation while this feature is in research preview. [Contact support](https://support.claude.com) if you need higher limits. --- title: Using agent memory url: https://platform.claude.com/docs/en/managed-agents/memory description: Give your agents persistent memory that survives across sessions using memory stores. --- Each Managed Agents session starts with a fresh context by default. When a session ends, any state the agent built up is gone. Memory stores let the agent carry information across sessions: user preferences, project conventions, prior mistakes, and domain context. Managed Agents API requests require the `managed-agents-2026-04-01` beta header, except memory store endpoints, which use `agent-memory-2026-07-22` instead. The SDK sets the correct beta header automatically. See [Beta headers](https://platform.claude.com/docs/en/api/beta-headers#endpoint-specific-headers). Don't combine `agent-memory-2026-07-22` with `managed-agents-2026-04-01` on a memory store request: sending both returns a `400` error. If your code sets beta headers explicitly, replace `managed-agents-2026-04-01` with `agent-memory-2026-07-22` on memory store calls rather than adding a second value. Session endpoints, including attaching a memory store to a session, still use `managed-agents-2026-04-01`. On July 22, 2026, the `managed-agents-2026-04-01` header adopts the same list behavior on `GET /v1/memory_stores/{memory_store_id}/memories`; sending `agent-memory-2026-07-22` opts you into that behavior now. Page cursors from requests made without the header aren't valid with it, so restart from the first page. ## Overview A **memory store** is a workspace-scoped collection of text documents optimized for Claude. When you attach a store to a session, it is mounted as a directory inside the session's sandbox. The agent reads and writes it with the same file tools it uses for the rest of the filesystem, and a note describing each mount is automatically added to the system prompt, telling the agent where to look. The [agent toolset](https://platform.claude.com/docs/en/managed-agents/tools) is required for these interactions; make sure to enable it during [agent creation](https://platform.claude.com/docs/en/managed-agents/agent-setup). Each **memory** in a store is addressed by a path and can be read and edited directly through the API or the Claude Console, allowing for tuning, importing, and exporting. Every change to a memory creates an immutable **memory version**, giving you an audit trail and point-in-time recovery for everything the agent writes. ## Create a memory store Give the store a `name` and a `description`. The description is passed to the agent, telling it what the store contains. ```bash cURL store=$(curl -s https://api.anthropic.com/v1/memory_stores \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: agent-memory-2026-07-22" \ -H "content-type: application/json" \ -d '{"name": "User Preferences", "description": "Per-user preferences and project context."}') store_id=$(jq -r '.id' <<< "$store") echo "$store_id" # memstore_01Hx... ``` ```bash CLI store_id=$(ant beta:memory-stores create \ --name "User Preferences" \ --description "Per-user preferences and project context." \ --transform id --raw-output) ``` ```python Python store = client.beta.memory_stores.create( name="User Preferences", description="Per-user preferences and project context.", ) print(store.id) # memstore_01Hx... ``` ```typescript TypeScript const store = await client.beta.memoryStores.create({ name: "User Preferences", description: "Per-user preferences and project context." }); console.log(store.id); // memstore_01Hx... ``` ```csharp C# var store = await client.Beta.MemoryStores.Create(new() { Name = "User Preferences", Description = "Per-user preferences and project context.", }); Console.WriteLine(store.ID); // memstore_01Hx... ``` ```go Go store, err := client.Beta.MemoryStores.New(ctx, anthropic.BetaMemoryStoreNewParams{ Name: "User Preferences", Description: anthropic.String("Per-user preferences and project context."), }) if err != nil { panic(err) } fmt.Println(store.ID) // memstore_01Hx... ``` ```java Java var store = client.beta().memoryStores().create( MemoryStoreCreateParams.builder() .name("User Preferences") .description("Per-user preferences and project context.") .build() ); IO.println(store.id()); // memstore_01Hx... ``` ```php PHP use Anthropic\Client; $client = new Client(); $store = $client->beta->memoryStores->create( name: 'User Preferences', description: 'Per-user preferences and project context.', ); echo "{$store->id}\n"; // memstore_01Hx... ``` ```ruby Ruby require "anthropic" client = Anthropic::Client.new store = client.beta.memory_stores.create( name: "User Preferences", description: "Per-user preferences and project context." ) puts store.id # memstore_01Hx... ``` The memory store `id` (`memstore_...`) is what you pass when attaching the store to a session. ### Seed it with content (optional) Pre-load a store with reference material before any agent runs: ```bash cURL curl -s "https://api.anthropic.com/v1/memory_stores/$store_id/memories" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: agent-memory-2026-07-22" \ -H "content-type: application/json" \ -d '{"path": "/formatting_standards.md", "content": "All reports use GAAP formatting. Dates are ISO-8601..."}' > /dev/null ``` ```bash CLI ant beta:memory-stores:memories create \ --memory-store-id "$store_id" \ --path "/formatting_standards.md" \ --content "All reports use GAAP formatting. Dates are ISO-8601..." \ > /dev/null ``` ```python Python client.beta.memory_stores.memories.create( store.id, path="/formatting_standards.md", content="All reports use GAAP formatting. Dates are ISO-8601...", ) ``` ```typescript TypeScript await client.beta.memoryStores.memories.create(store.id, { path: "/formatting_standards.md", content: "All reports use GAAP formatting. Dates are ISO-8601..." }); ``` ```csharp C# await client.Beta.MemoryStores.Memories.Create(store.ID, new() { Path = "/formatting_standards.md", Content = "All reports use GAAP formatting. Dates are ISO-8601...", }); ``` ```go Go _, err = client.Beta.MemoryStores.Memories.New(ctx, store.ID, anthropic.BetaMemoryStoreMemoryNewParams{ Path: "/formatting_standards.md", Content: anthropic.String("All reports use GAAP formatting. Dates are ISO-8601..."), }) if err != nil { panic(err) } ``` ```java Java client.beta().memoryStores().memories().create( store.id(), MemoryCreateParams.builder() .path("/formatting_standards.md") .content("All reports use GAAP formatting. Dates are ISO-8601...") .build() ); ``` ```php PHP $client->beta->memoryStores->memories->create( $store->id, path: '/formatting_standards.md', content: 'All reports use GAAP formatting. Dates are ISO-8601...', ); ``` ```ruby Ruby client.beta.memory_stores.memories.create( store.id, path: "/formatting_standards.md", content: "All reports use GAAP formatting. Dates are ISO-8601..." ) ``` Individual memories within the store are capped at 100 kB (\~25k tokens). A store holds a maximum of 2,000 memories. Structure memory as many small focused files, not a few large ones. ## Attach a memory store to a session Memory stores are attached in the session's `resources[]` array when the [session is created](https://platform.claude.com/docs/en/managed-agents/sessions#creating-a-session). Unlike file resources, memory stores can only be attached at session creation time; adding or removing one from a running session is not supported. Optionally include `instructions` to provide session-specific guidance for how the agent should use this store. It is shown to the agent alongside the store's `name` and `description`, and is capped at 4,096 characters. You can configure `access` as well. It defaults to `read_write` (shown explicitly in the following example), but `read_only` is also supported. ```bash cURL curl -s https://api.anthropic.com/v1/sessions \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ --data @- <beta->sessions->create( agent: $agent->id, environmentID: $environment->id, resources: [ [ 'type' => 'memory_store', 'memory_store_id' => $store->id, 'access' => 'read_write', 'instructions' => 'User preferences and project context. Check before starting any task.', ], ], ); ``` ```ruby Ruby session = client.beta.sessions.create( agent: agent.id, environment_id: environment.id, resources: [ { type: "memory_store", memory_store_id: store.id, access: "read_write", instructions: "User preferences and project context. Check before starting any task." } ] ) ``` Memory stores attach with `read_write` access by default. If the agent processes untrusted input (user-supplied prompts, fetched web content, or third-party tool output), a successful prompt injection could write malicious content into the store. Later sessions then read that content as trusted memory. Use `read_only` for reference material, shared lookups, and any store the agent does not need to modify. A maximum of **8 memory stores** are supported per session. Attach multiple stores when different parts of memory have different owners or access rules. Common reasons: * **Shared reference material:** one read-only store attached to many sessions (standards, conventions, domain knowledge), kept separate from each session's own read-write store. * **Mapping to your product's structure:** one store per end user, per team, or per project, while sharing a single agent configuration. * **Different lifecycles:** a store that outlives any single session, or one you want to archive on its own schedule. ### How the agent accesses memory Each attached store is mounted inside the session's sandbox as a directory under `/mnt/memory/`. The directory name is the store's display name sanitized to a filesystem-safe slug (lowercased; non-alphanumeric runs become a single hyphen), so a store named "Demo Memory" mounts at `/mnt/memory/demo-memory/`. The exact path is returned in the `mount_path` field on the session's memory-store resource; read it from there rather than constructing it yourself. The agent reads and writes the store with the standard [agent toolset](https://platform.claude.com/docs/en/managed-agents/tools). Writes under the mount path are persisted back to the store and stay in sync across sessions that share it; writes to any other path under `/mnt/memory/` land in container-local scratch and are lost when the session ends. A short description of each mount (display name, mount path, access mode, store `description`, and any `instructions`) is automatically added to the system prompt. `access` is enforced at the filesystem level: a `read_only` mount rejects writes, while writes to a `read_write` mount produce [memory versions](https://platform.claude.com/docs/en/managed-agents/memory#audit-memory-changes) attributed to the session. The agent's reads and writes appear in the [event stream](https://platform.claude.com/docs/en/managed-agents/events-and-streaming) as ordinary `agent.tool_use` and `agent.tool_result` events for whichever tool touched the mount. ## View and edit memories Memory stores can be managed directly through the API. Use this for building review workflows, correcting bad memories, or seeding stores before any session runs. ### List memories List the memories in a store. Results are returned in a stable, server-defined order. * `path_prefix` scopes the list to one directory. It must end with `/` and matches whole path segments, so `path_prefix=/notes/` returns `/notes/todo.md` but not `/notes-archive/todo.md`. * `depth` controls how deep the listing goes below `path_prefix`: omit it (or pass `0`) to list the whole subtree, or pass `1` to list only the immediate children. Other values return a `400` error. ```bash cURL curl -s "https://api.anthropic.com/v1/memory_stores/$store_id/memories?path_prefix=/" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: agent-memory-2026-07-22" | jq -r '.data[] | "\(.type) \(.path)"' ``` ```bash CLI ant beta:memory-stores:memories list \ --memory-store-id "$store_id" \ --path-prefix "/" ``` ```python Python page = client.beta.memory_stores.memories.list( store.id, path_prefix="/", ) for item in page.data: print(item.type, item.path) ``` ```typescript TypeScript const page = await client.beta.memoryStores.memories.list(store.id, { path_prefix: "/" }); for (const item of page.data) { console.log(item.type, item.path); } ``` ```csharp C# var page = await client.Beta.MemoryStores.Memories.List(store.ID, new() { PathPrefix = "/", }); await foreach (var item in page.Paginate()) { var line = item.Match(m => $"memory {m.Path}", p => $"memory_prefix {p.Path}"); Console.WriteLine(line); } ``` ```go Go page, err := client.Beta.MemoryStores.Memories.List(ctx, store.ID, anthropic.BetaMemoryStoreMemoryListParams{ PathPrefix: anthropic.String("/"), }) if err != nil { panic(err) } for _, item := range page.Data { fmt.Println(item.Type, item.Path) } ``` ```java Java var page = client.beta().memoryStores().memories().list( store.id(), MemoryListParams.builder() .pathPrefix("/") .build() ); for (var item : page.data()) { item.memory().ifPresent(m -> IO.println("memory " + m.path())); item.memoryPrefix().ifPresent(p -> IO.println("memory_prefix " + p.path())); } ``` ```php PHP $page = $client->beta->memoryStores->memories->list( $store->id, pathPrefix: '/', ); foreach ($page->data as $item) { echo "{$item->type} {$item->path}\n"; } ``` ```ruby Ruby page = client.beta.memory_stores.memories.list( store.id, path_prefix: "/" ) page.data.each do |entry| puts "#{entry.type} #{entry.path}" end ``` See the [List memories reference](https://platform.claude.com/docs/en/api/beta/memory_stores/memories/list) for full parameters and response schema. ### Read a memory Fetching an individual memory returns the full content. ```bash cURL curl -s "https://api.anthropic.com/v1/memory_stores/$store_id/memories/$mem_id" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: agent-memory-2026-07-22" | jq -r '.content' ``` ```bash CLI ant beta:memory-stores:memories retrieve \ --memory-store-id "$store_id" \ --memory-id "$mem_id" ``` ```python Python retrieved = client.beta.memory_stores.memories.retrieve( mem.id, memory_store_id=store.id, ) print(retrieved.content) ``` ```typescript TypeScript const retrieved = await client.beta.memoryStores.memories.retrieve(mem.id, { memory_store_id: store.id }); console.log(retrieved.content); ``` ```csharp C# var retrieved = await client.Beta.MemoryStores.Memories.Retrieve(mem.ID, new() { MemoryStoreID = store.ID, }); Console.WriteLine(retrieved.Content); ``` ```go Go retrieved, err := client.Beta.MemoryStores.Memories.Get(ctx, mem.ID, anthropic.BetaMemoryStoreMemoryGetParams{ MemoryStoreID: store.ID, }) if err != nil { panic(err) } fmt.Println(retrieved.Content) ``` ```java Java var retrieved = client.beta().memoryStores().memories().retrieve( mem.id(), MemoryRetrieveParams.builder().memoryStoreId(store.id()).build() ); IO.println(retrieved.content().orElseThrow()); ``` ```php PHP $retrieved = $client->beta->memoryStores->memories->retrieve($mem->id, memoryStoreID: $store->id); echo "{$retrieved->content}\n"; ``` ```ruby Ruby retrieved = client.beta.memory_stores.memories.retrieve( mem.id, memory_store_id: store.id ) puts retrieved.content ``` See the [Retrieve a memory reference](https://platform.claude.com/docs/en/api/beta/memory_stores/memories/retrieve) for full parameters and response schema. ### Create a memory `memories.create` creates a memory at a given `path`. Create does not overwrite; to change an existing memory, use [`memories.update`](https://platform.claude.com/docs/en/managed-agents/memory#update-a-memory). ```bash cURL mem=$(curl -s "https://api.anthropic.com/v1/memory_stores/$store_id/memories" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: agent-memory-2026-07-22" \ -H "content-type: application/json" \ -d '{"path": "/preferences/formatting.md", "content": "Always use tabs, not spaces."}') mem_id=$(jq -r '.id' <<< "$mem") mem_sha=$(jq -r '.content_sha256' <<< "$mem") ``` ```bash CLI mem=$(ant beta:memory-stores:memories create \ --memory-store-id "$store_id" \ --path "/preferences/formatting.md" \ --content "Always use tabs, not spaces." \ --format json) mem_id=$(jq -r '.id' <<< "$mem") mem_sha=$(jq -r '.content_sha256' <<< "$mem") ``` ```python Python mem = client.beta.memory_stores.memories.create( store.id, path="/preferences/formatting.md", content="Always use tabs, not spaces.", ) ``` ```typescript TypeScript const mem = await client.beta.memoryStores.memories.create(store.id, { path: "/preferences/formatting.md", content: "Always use tabs, not spaces." }); ``` ```csharp C# var mem = await client.Beta.MemoryStores.Memories.Create(store.ID, new() { Path = "/preferences/formatting.md", Content = "Always use tabs, not spaces.", }); ``` ```go Go mem, err := client.Beta.MemoryStores.Memories.New(ctx, store.ID, anthropic.BetaMemoryStoreMemoryNewParams{ Path: "/preferences/formatting.md", Content: anthropic.String("Always use tabs, not spaces."), }) if err != nil { panic(err) } ``` ```java Java var mem = client.beta().memoryStores().memories().create( store.id(), MemoryCreateParams.builder() .path("/preferences/formatting.md") .content("Always use tabs, not spaces.") .build() ); ``` ```php PHP $mem = $client->beta->memoryStores->memories->create( $store->id, path: '/preferences/formatting.md', content: 'Always use tabs, not spaces.', ); ``` ```ruby Ruby mem = client.beta.memory_stores.memories.create( store.id, path: "/preferences/formatting.md", content: "Always use tabs, not spaces." ) ``` See the [Create a memory reference](https://platform.claude.com/docs/en/api/beta/memory_stores/memories/create) for full parameters and response schema. ### Update a memory `memories.update` modifies an existing memory by ID. You can change `content`, `path` (a rename), or both. The example renames a memory to an archive path: ```bash cURL curl -s -X POST "https://api.anthropic.com/v1/memory_stores/$store_id/memories/$mem_id" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: agent-memory-2026-07-22" \ -H "content-type: application/json" \ -d '{"path": "/archive/2026_q1_formatting.md"}' > /dev/null ``` ```bash CLI ant beta:memory-stores:memories update \ --memory-store-id "$store_id" \ --memory-id "$mem_id" \ --path "/archive/2026_q1_formatting.md" \ > /dev/null ``` ```python Python client.beta.memory_stores.memories.update( mem.id, memory_store_id=store.id, path="/archive/2026_q1_formatting.md", ) ``` ```typescript TypeScript await client.beta.memoryStores.memories.update(mem.id, { memory_store_id: store.id, path: "/archive/2026_q1_formatting.md" }); ``` ```csharp C# await client.Beta.MemoryStores.Memories.Update(mem.ID, new() { MemoryStoreID = store.ID, Path = "/archive/2026_q1_formatting.md", }); ``` ```go Go _, err = client.Beta.MemoryStores.Memories.Update(ctx, mem.ID, anthropic.BetaMemoryStoreMemoryUpdateParams{ MemoryStoreID: store.ID, Path: anthropic.String("/archive/2026_q1_formatting.md"), }) if err != nil { panic(err) } ``` ```java Java client.beta().memoryStores().memories().update( mem.id(), MemoryUpdateParams.builder() .memoryStoreId(store.id()) .path("/archive/2026_q1_formatting.md") .build() ); ``` ```php PHP $client->beta->memoryStores->memories->update( $mem->id, memoryStoreID: $store->id, path: '/archive/2026_q1_formatting.md', ); ``` ```ruby Ruby client.beta.memory_stores.memories.update( mem.id, memory_store_id: store.id, path: "/archive/2026_q1_formatting.md" ) ``` See the [Update a memory reference](https://platform.claude.com/docs/en/api/beta/memory_stores/memories/update) for full parameters and response schema. #### Safe content edits (optimistic concurrency) To avoid clobbering a concurrent write, pass a `content_sha256` precondition. The update only applies if the stored content hash still matches the one you read; on mismatch, re-read the memory and retry against the fresh state. ```bash cURL curl -s -X POST "https://api.anthropic.com/v1/memory_stores/$store_id/memories/$mem_id" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: agent-memory-2026-07-22" \ -H "content-type: application/json" \ --data @- > /dev/null < /dev/null ``` ```python Python client.beta.memory_stores.memories.update( memory_id=mem.id, memory_store_id=store.id, content="CORRECTED: Always use 2-space indentation.", precondition={"type": "content_sha256", "content_sha256": mem.content_sha256}, ) ``` ```typescript TypeScript await client.beta.memoryStores.memories.update(mem.id, { memory_store_id: store.id, content: "CORRECTED: Always use 2-space indentation.", precondition: { type: "content_sha256", content_sha256: mem.content_sha256 } }); ``` ```csharp C# await client.Beta.MemoryStores.Memories.Update(mem.ID, new() { MemoryStoreID = store.ID, Content = "CORRECTED: Always use 2-space indentation.", Precondition = new BetaManagedAgentsPrecondition { Type = "content_sha256", ContentSha256 = mem.ContentSha256, }, }); ``` ```go Go _, err = client.Beta.MemoryStores.Memories.Update(ctx, mem.ID, anthropic.BetaMemoryStoreMemoryUpdateParams{ MemoryStoreID: store.ID, Content: anthropic.String("CORRECTED: Always use 2-space indentation."), Precondition: anthropic.BetaManagedAgentsPreconditionParam{ Type: anthropic.BetaManagedAgentsPreconditionTypeContentSha256, ContentSha256: anthropic.String(mem.ContentSha256), }, }) if err != nil { panic(err) } ``` ```java Java client.beta().memoryStores().memories().update( mem.id(), MemoryUpdateParams.builder() .memoryStoreId(store.id()) .content("CORRECTED: Always use 2-space indentation.") .precondition( BetaManagedAgentsPrecondition.builder() .type(BetaManagedAgentsPrecondition.Type.CONTENT_SHA256) .contentSha256(mem.contentSha256()) .build() ) .build() ); ``` ```php PHP $client->beta->memoryStores->memories->update( $mem->id, memoryStoreID: $store->id, content: 'CORRECTED: Always use 2-space indentation.', precondition: ['type' => 'content_sha256', 'content_sha256' => $mem->contentSha256], ); ``` ```ruby Ruby client.beta.memory_stores.memories.update( mem.id, memory_store_id: store.id, content: "CORRECTED: Always use 2-space indentation.", precondition: {type: "content_sha256", content_sha256: mem.content_sha256} ) ``` ### Delete a memory ```bash cURL curl -s -X DELETE "https://api.anthropic.com/v1/memory_stores/$store_id/memories/$mem_id" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: agent-memory-2026-07-22" > /dev/null ``` ```bash CLI ant beta:memory-stores:memories delete \ --memory-store-id "$store_id" \ --memory-id "$mem_id" \ > /dev/null ``` ```python Python client.beta.memory_stores.memories.delete( mem.id, memory_store_id=store.id, ) ``` ```typescript TypeScript await client.beta.memoryStores.memories.delete(mem.id, { memory_store_id: store.id }); ``` ```csharp C# await client.Beta.MemoryStores.Memories.Delete(mem.ID, new() { MemoryStoreID = store.ID, }); ``` ```go Go _, err = client.Beta.MemoryStores.Memories.Delete(ctx, mem.ID, anthropic.BetaMemoryStoreMemoryDeleteParams{ MemoryStoreID: store.ID, }) if err != nil { panic(err) } ``` ```java Java client.beta().memoryStores().memories().delete( mem.id(), MemoryDeleteParams.builder().memoryStoreId(store.id()).build() ); ``` ```php PHP $client->beta->memoryStores->memories->delete($mem->id, memoryStoreID: $store->id); ``` ```ruby Ruby client.beta.memory_stores.memories.delete( mem.id, memory_store_id: store.id ) ``` See the [Delete a memory reference](https://platform.claude.com/docs/en/api/beta/memory_stores/memories/delete) for full parameters and response schema. ## Audit memory changes Every mutation to a memory creates an immutable **memory version** (`memver_...`). Use the version endpoints to audit who changed what and when, to inspect or restore a prior snapshot, and to scrub sensitive content out of history with redact. Versions belong to the store (not the individual memory) and survive even after the memory itself is deleted, so the audit trail stays complete. Versions are retained for 30 days; however, the recent versions are always kept regardless of age, so memories that change infrequently might retain history beyond 30 days. The live `memories.retrieve` call always returns the latest version; the version endpoints give you the retained history. There is no dedicated restore endpoint; to roll back, retrieve the version you want and write its `content` back with `memories.update` (or `memories.create` if the parent memory has been deleted, because versions outlive their parent). Past memory versions might be deleted after 30 days. To preserve memory history for longer, export versions through the API. ### List versions List version history for a store, newest first. The example filters to a single memory's history: ```bash cURL versions=$(curl -s "https://api.anthropic.com/v1/memory_stores/$store_id/memory_versions?memory_id=$mem_id" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: agent-memory-2026-07-22") jq -r '.data[] | "\(.id): \(.operation)"' <<< "$versions" version_id=$(jq -r '.data[1].id' <<< "$versions") ``` ```bash CLI versions=$(ant beta:memory-stores:memory-versions list \ --memory-store-id "$store_id" \ --memory-id "$mem_id" \ --format json) # `list --format json` emits one JSON object per item. jq -r '"\(.id): \(.operation)"' <<< "$versions" version_id=$(jq -rs '.[1].id' <<< "$versions") ``` ```python Python versions = client.beta.memory_stores.memory_versions.list( store.id, memory_id=mem.id, ) for version in versions: print(f"{version.id}: {version.operation}") version_id = versions.data[1].id ``` ```typescript TypeScript const versions = await client.beta.memoryStores.memoryVersions.list(store.id, { memory_id: mem.id }); for await (const v of versions) { console.log(`${v.id}: ${v.operation}`); } const versionId = versions.data[1].id; ``` ```csharp C# var versions = await client.Beta.MemoryStores.MemoryVersions.List(store.ID, new() { MemoryID = mem.ID, }); var versionIds = new List(); await foreach (var v in versions.Paginate()) { Console.WriteLine($"{v.ID}: {v.Operation.Raw()}"); versionIds.Add(v.ID); } var versionId = versionIds[1]; ``` ```go Go versions := client.Beta.MemoryStores.MemoryVersions.ListAutoPaging(ctx, store.ID, anthropic.BetaMemoryStoreMemoryVersionListParams{ MemoryID: anthropic.String(mem.ID), }) for versions.Next() { v := versions.Current() fmt.Printf("%s: %s\n", v.ID, v.Operation) } if err := versions.Err(); err != nil { panic(err) } vpage, err := client.Beta.MemoryStores.MemoryVersions.List(ctx, store.ID, anthropic.BetaMemoryStoreMemoryVersionListParams{ MemoryID: anthropic.String(mem.ID), }) if err != nil { panic(err) } versionID := vpage.Data[1].ID ``` ```java Java var versions = client.beta().memoryStores().memoryVersions().list( store.id(), MemoryVersionListParams.builder().memoryId(mem.id()).build() ); for (var v : versions.autoPager()) { IO.println(v.id() + ": " + v.operation()); } var versionId = versions.data().get(1).id(); ``` ```php PHP $versions = $client->beta->memoryStores->memoryVersions->list( $store->id, memoryID: $mem->id, ); foreach ($versions->pagingEachItem() as $v) { echo "{$v->id}: {$v->operation}\n"; } $versionId = $versions->data[1]->id; ``` ```ruby Ruby versions = client.beta.memory_stores.memory_versions.list( store.id, memory_id: mem.id ) versions.auto_paging_each do |version| puts "#{version.id}: #{version.operation}" end version_id = versions.data[1].id ``` See the [List memory versions reference](https://platform.claude.com/docs/en/api/beta/memory_stores/memory_versions/list) for full parameters and response schema. ### Retrieve a version Fetching an individual version returns the same fields as the list response plus the full `content` body. ```bash cURL curl -s "https://api.anthropic.com/v1/memory_stores/$store_id/memory_versions/$version_id" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: agent-memory-2026-07-22" ``` ```bash CLI ant beta:memory-stores:memory-versions retrieve \ --memory-store-id "$store_id" \ --memory-version-id "$version_id" ``` ```python Python version = client.beta.memory_stores.memory_versions.retrieve( version_id, memory_store_id=store.id, ) print(version.content) ``` ```typescript TypeScript const version = await client.beta.memoryStores.memoryVersions.retrieve(versionId, { memory_store_id: store.id }); console.log(version.content); ``` ```csharp C# var version = await client.Beta.MemoryStores.MemoryVersions.Retrieve(versionId, new() { MemoryStoreID = store.ID, }); Console.WriteLine(version.Content); ``` ```go Go version, err := client.Beta.MemoryStores.MemoryVersions.Get(ctx, versionID, anthropic.BetaMemoryStoreMemoryVersionGetParams{ MemoryStoreID: store.ID, }) if err != nil { panic(err) } fmt.Println(version.Content) ``` ```java Java var version = client.beta().memoryStores().memoryVersions().retrieve( versionId, MemoryVersionRetrieveParams.builder().memoryStoreId(store.id()).build() ); IO.println(version.content().orElseThrow()); ``` ```php PHP $version = $client->beta->memoryStores->memoryVersions->retrieve( $versionId, memoryStoreID: $store->id, ); echo "{$version->content}\n"; ``` ```ruby Ruby version = client.beta.memory_stores.memory_versions.retrieve( version_id, memory_store_id: store.id ) puts version.content ``` See the [Retrieve a memory version reference](https://platform.claude.com/docs/en/api/beta/memory_stores/memory_versions/retrieve) for full parameters and response schema. ### Redact a version Redact scrubs content out of a historical version while preserving the audit trail (who did what, when). Use it for compliance workflows such as removing leaked secrets, PII, or user deletion requests. A version that is the current head of a live memory cannot be redacted. Write a new version first (or delete the memory), then redact the old one. ```bash cURL curl -s -X POST "https://api.anthropic.com/v1/memory_stores/$store_id/memory_versions/$version_id/redact" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: agent-memory-2026-07-22" \ -H "content-type: application/json" \ -d '{}' ``` ```bash CLI ant beta:memory-stores:memory-versions redact \ --memory-store-id "$store_id" \ --memory-version-id "$version_id" ``` ```python Python client.beta.memory_stores.memory_versions.redact( version_id, memory_store_id=store.id, ) ``` ```typescript TypeScript await client.beta.memoryStores.memoryVersions.redact(versionId, { memory_store_id: store.id }); ``` ```csharp C# await client.Beta.MemoryStores.MemoryVersions.Redact(versionId, new() { MemoryStoreID = store.ID, }); ``` ```go Go _, err = client.Beta.MemoryStores.MemoryVersions.Redact(ctx, versionID, anthropic.BetaMemoryStoreMemoryVersionRedactParams{ MemoryStoreID: store.ID, }) if err != nil { panic(err) } ``` ```java Java client.beta().memoryStores().memoryVersions().redact( versionId, MemoryVersionRedactParams.builder().memoryStoreId(store.id()).build() ); ``` ```php PHP $client->beta->memoryStores->memoryVersions->redact( $versionId, memoryStoreID: $store->id, ); ``` ```ruby Ruby client.beta.memory_stores.memory_versions.redact( version_id, memory_store_id: store.id ) ``` See the [Redact a memory version reference](https://platform.claude.com/docs/en/api/beta/memory_stores/memory_versions/redact) for full parameters and response schema. ## Manage memory stores In addition to [`create`](https://platform.claude.com/docs/en/api/beta/memory_stores/create), memory stores support [`retrieve`](https://platform.claude.com/docs/en/api/beta/memory_stores/retrieve), [`update`](https://platform.claude.com/docs/en/api/beta/memory_stores/update), [`list`](https://platform.claude.com/docs/en/api/beta/memory_stores/list), [`archive`](https://platform.claude.com/docs/en/api/beta/memory_stores/archive), and [`delete`](https://platform.claude.com/docs/en/api/beta/memory_stores/delete). ### List stores List stores in the workspace. Archived stores are excluded by default; pass `include_archived: true` to include them. ```bash cURL curl -s "https://api.anthropic.com/v1/memory_stores?include_archived=true" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: agent-memory-2026-07-22" | jq '.data[] | {id, name, archived_at}' ``` ```bash CLI ant beta:memory-stores list --include-archived ``` ```python Python for memory_store in client.beta.memory_stores.list(include_archived=True): print(memory_store.id, memory_store.name, memory_store.archived_at) ``` ```typescript TypeScript for await (const s of client.beta.memoryStores.list({ include_archived: true })) { console.log(s.id, s.name, s.archived_at); } ``` ```csharp C# var stores = await client.Beta.MemoryStores.List(new() { IncludeArchived = true }); await foreach (var s in stores.Paginate()) { Console.WriteLine($"{s.ID} {s.Name} {s.ArchivedAt}"); } ``` ```go Go stores := client.Beta.MemoryStores.ListAutoPaging(ctx, anthropic.BetaMemoryStoreListParams{ IncludeArchived: anthropic.Bool(true), }) for stores.Next() { s := stores.Current() fmt.Println(s.ID, s.Name, s.ArchivedAt) } if err := stores.Err(); err != nil { panic(err) } ``` ```java Java for (var s : client.beta().memoryStores().list( MemoryStoreListParams.builder().includeArchived(true).build() ).autoPager()) { IO.println(s.id() + " " + s.name() + " " + s.archivedAt()); } ``` ```php PHP foreach ($client->beta->memoryStores->list(includeArchived: true)->pagingEachItem() as $s) { // archivedAt is only set on archived stores. $archivedAt = isset($s->archivedAt) ? $s->archivedAt->format(DATE_ATOM) : ''; echo "{$s->id} {$s->name} {$archivedAt}\n"; } ``` ```ruby Ruby client.beta.memory_stores.list(include_archived: true).auto_paging_each do |memory_store| puts "#{memory_store.id} #{memory_store.name} #{memory_store.archived_at}" end ``` See the [List memory stores reference](https://platform.claude.com/docs/en/api/beta/memory_stores/list) for full parameters and response schema. ### Archive a store Archiving makes a store read-only and prevents it from being attached to new sessions. Archiving is one-way; there is no unarchive. ```bash cURL curl -s -X POST "https://api.anthropic.com/v1/memory_stores/$store_id/archive" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: agent-memory-2026-07-22" > /dev/null ``` ```bash CLI ant beta:memory-stores archive --memory-store-id "$store_id" ``` ```python Python client.beta.memory_stores.archive(store.id) ``` ```typescript TypeScript await client.beta.memoryStores.archive(store.id); ``` ```csharp C# await client.Beta.MemoryStores.Archive(store.ID); ``` ```go Go _, err = client.Beta.MemoryStores.Archive(ctx, store.ID, anthropic.BetaMemoryStoreArchiveParams{}) if err != nil { panic(err) } ``` ```java Java client.beta().memoryStores().archive(store.id()); ``` ```php PHP $client->beta->memoryStores->archive($store->id); ``` ```ruby Ruby client.beta.memory_stores.archive(store.id) ``` See the [Archive a memory store reference](https://platform.claude.com/docs/en/api/beta/memory_stores/archive) for full parameters and response schema. To permanently remove a store along with all of its memories and versions, use [`memory_stores.delete`](https://platform.claude.com/docs/en/api/beta/memory_stores/delete). ## Best practices for memory management When a store reaches its 2,000-memory limit, writes to new memories fail: both direct `memories.create` calls and the agent's file writes to unmapped paths. Existing memories remain readable and editable. The following practices help you stay well under the limit and recover gracefully if you reach it. * **Use focused stores.** Rather than one large general-purpose store, use smaller purpose-built stores: one per user, one for shared domain knowledge, and one for project-specific context. Each store has its own 2,000-memory limit, so keeping stores scoped reduces the chance any single one fills up. * **Condense or prune before the store fills up.** Delete stale or redundant memories with `memories.delete`. You can also run a [dreaming session](https://platform.claude.com/docs/en/managed-agents/dreams), which consolidates fragmented content into a separate new output store rather than modifying the original. Switch your sessions over to that output store, then archive or delete the original. * **Attach a new store when it makes sense.** If a store has grown beyond its useful scope, attach a fresh one for new content and attach the original with `read_only` access. The agent can read from both while only writing to the new one. * **Limit write access where appropriate.** Sessions that only read shared reference material don't need `read_write`. Keeping write access scoped to sessions that actually add new memories makes it easier to track where growth is coming from. ### Advanced orchestration --- title: Multiagent orchestration url: https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration description: Coordinate multiple agents within a single session. --- Multiagent orchestration lets one agent coordinate with others to complete complex work. Agents can act in parallel with their own isolated context, which helps improve output quality and can also improve time to completion. Not sure a multiagent setup fits your problem? See [when to use multiagent systems (and when not to)](https://claude.com/blog/building-multi-agent-systems-when-and-how-to-use-them). Managed Agents API requests require the `managed-agents-2026-04-01` beta header, except memory store endpoints, which use `agent-memory-2026-07-22` instead. The SDK sets the correct beta header automatically. See [Beta headers](https://platform.claude.com/docs/en/api/beta-headers#endpoint-specific-headers). ## How it works All agents share the same sandbox, filesystem, and [vault credentials](https://platform.claude.com/docs/en/managed-agents/vaults), but each agent runs in its own **session thread**, a context-isolated event stream with its own conversation history. The coordinator reports activity in the **primary thread** (which is the same as the session-level [event stream](https://platform.claude.com/docs/en/managed-agents/events-and-streaming)); additional threads are spawned at runtime when the coordinator delegates work. Threads are persistent: the coordinator can send a follow-up to an agent it called earlier, and that agent retains everything from its previous turns. Each agent uses its own configuration: model, system prompt, tools, MCP servers, and skills. Session-level [agent configuration overrides](https://platform.claude.com/docs/en/managed-agents/sessions#override-agent-configuration-for-a-session) are the exception; they apply to the coordinator and its `self` copies. Tools, MCP servers, and context are not shared. ### What to delegate Multiagent coordination is best suited for complex tasks that either require work across a variety of surfaces, or where multiple well-scoped tasks contribute to an overall goal. Patterns that work well: * **Parallelization:** Fan out independent subtasks simultaneously (searching multiple sources, analyzing separate files) and have the coordinator synthesize the results. * **Specialization:** Route to agents with domain-focused system prompts and tools, such as a security agent or a documentation agent, rather than loading a single agent with every capability. * **Escalation:** Consult a more capable agent or model for a subset of complex subtasks. ## Configure the coordinator When [defining your agent](https://platform.claude.com/docs/en/managed-agents/agent-setup), set `multiagent` to declare the roster of agents the coordinator can delegate to: ```bash cURL coordinator=$(curl -fsS https://api.anthropic.com/v1/agents \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d @- <beta->agents->create( name: 'Engineering Lead', model: 'claude-opus-5', system: 'You coordinate engineering work. Delegate code review to the reviewer agent and test writing to the test agent.', tools: [ ['type' => 'agent_toolset_20260401'], ], multiagent: [ 'type' => 'coordinator', 'agents' => [ ['type' => 'agent', 'id' => $reviewerAgent->id], ['type' => 'agent', 'id' => $testWriterAgent->id], ], ], ); ``` ```ruby Ruby coordinator = client.beta.agents.create( name: "Engineering Lead", model: "claude-opus-5", system: "You coordinate engineering work. Delegate code review to the reviewer agent and test writing to the test agent.", tools: [ {type: "agent_toolset_20260401"} ], multiagent: { type: "coordinator", agents: [ {type: "agent", id: reviewer_agent.id}, {type: "agent", id: test_writer_agent.id} ] } ) ``` `multiagent.agents` can accept any of the following: * `{"type": "agent", "id": agent.id}` references a previously created `agent` by ID. If no `version` is specified, the reference is pinned to the latest version of that agent at the time the coordinator is created. * `{"type": "agent", "id": agent.id, "version": agent.version}` pins a specific agent version. * `{"type": "self"}` allows the coordinator to spawn copies of itself. If the session was created with [agent configuration overrides](https://platform.claude.com/docs/en/managed-agents/sessions#override-agent-configuration-for-a-session), those overrides also apply to these copies; roster entries referenced by ID are unaffected. * `{"type": "advisor", "model": ""}` gives the session's primary thread an advisor it can consult mid-turn. At most one advisor entry per roster. See [Give the session an advisor](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration#give-the-session-an-advisor). The coordinator's configuration, including its `multiagent.agents` roster, is snapshotted when the coordinator is created or updated. Referenced agents stay pinned to the versions resolved at that time and do not automatically pick up later updates to their definitions. To delegate to a newer version of a referenced agent, [update the coordinator](https://platform.claude.com/docs/en/managed-agents/agent-setup#update-an-agent) so its roster references that version. The coordinator can only delegate to one level of agents; referencing an agent that has its own `multiagent.agents` roster fails the create or update request with a validation error. A maximum of 20 unique agents can be listed in `multiagent.agents`, but the coordinator can call multiple copies of each agent. When agents pin an [inference geography](https://platform.claude.com/docs/en/manage-claude/data-residency) (`model.inference_geo` in the [agent definition](https://platform.claude.com/docs/en/managed-agents/agent-setup)), the coordinator's pin and every roster member's pin must either all be set to the same value or all be unset. A mismatched roster is rejected with a 400 validation error, both when the agent is saved and when a [session-create override](https://platform.claude.com/docs/en/managed-agents/sessions#override-agent-configuration-for-a-session) changes any of the pins. ### Give the session an advisor An advisor entry in `multiagent.agents` gives the session's primary thread an **advisor**: a model it can consult mid-turn for strategic guidance, such as planning an approach, getting unstuck, or reviewing work before finishing. The entry has exactly two fields, `type` and `model`: ```bash cURL curl -fsS https://api.anthropic.com/v1/agents \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d '{ "name": "Backend engineer", "model": "claude-sonnet-5", "system": "You implement backend features end to end. Consult the advisor before major backend design decisions.", "multiagent": { "type": "coordinator", "agents": [ {"type": "advisor", "model": "claude-opus-5"} ] } }' ``` A roster can contain at most one advisor entry, alongside any of the other roster forms. The entry occupies the reserved roster name `anthropic.advisor`: a roster that lists both an advisor entry and a member literally named `anthropic.advisor` is rejected with a 400 validation error. In responses, the advisor entry is echoed last in the roster regardless of the position it was submitted in. The advisor model must meet a minimum capability bar, and the agent's own model must not be more capable than its advisor; models of equal capability can pair. An invalid pairing is rejected with a 400 validation error when the agent is saved. Valid pairings follow the advisor tool's [model compatibility](https://platform.claude.com/docs/en/agents-and-tools/tool-use/advisor-tool#model-compatibility) table. The advisor is also available as a [server tool on the Messages API](https://platform.claude.com/docs/en/agents-and-tools/tool-use/advisor-tool). The Managed Agents surface differs in configuration and delivery: the roster entry has no `max_uses`, `max_tokens`, or `caching` fields, and advice arrives through thread events rather than `advisor_tool_result` blocks. #### How consultations work Each consultation runs as a platform-spawned thread named `anthropic.advisor` that terminates itself when the consultation completes, and the advice is delivered to the primary thread as an `agent.thread_message_received` event. A consultation emits the standard thread events, identified by the reserved name `anthropic.advisor` (the thread lifecycle events carry it as `agent_name`, and the advice delivery carries it as `from_agent_name`), typically in this order: 1. `session.thread_created` 2. `session.thread_status_running` 3. `agent.thread_message_received` (the advice) 4. `session.thread_status_idle` (`stop_reason: end_turn`) 5. `session.thread_status_terminated` No `agent.tool_use` events are emitted for a consultation, and no `agent.thread_message_sent` event appears on the session's event stream, because the consultation input is composed by the platform rather than sent by the agent. If you list the advisor thread's own events, the advice also appears there as an `agent.thread_message_sent` event. The advice delivery (event 3) is not guaranteed to arrive before the advisor thread's idle and terminated events, so don't treat those as a signal that the advice has already been delivered. Whether your client can read the advice is the advisor model's policy, and it mirrors the [result variants](https://platform.claude.com/docs/en/agents-and-tools/tool-use/advisor-tool#result-variants) split on the Messages API advisor tool. Advisor models that return plaintext results there deliver the advice as readable text content here; advisor models that return redacted results there deliver a `[{"type": "redacted"}]` placeholder as the message content on every client surface, while the agent itself still reads the full advice server-side. In the preceding example, Claude Opus 5 is a redacted-result advisor, so your client sees the placeholder while the agent reads the full advice; choose Claude Opus 4.8 as the advisor instead if you want the advice readable on the event stream. Advisor thinking is never surfaced. Clients cannot send `redacted` blocks themselves; an event containing one is rejected with a 400 validation error. A failed or interrupted consultation never fails the agent's turn: the agent continues after a generic notice that the consultation failed. A session-level `user.interrupt` during a consultation terminates the advisor thread with no advice delivered; a `user.interrupt` with the advisor thread's `session_thread_id` abandons only that consultation. #### Advisor threads The advisor is not a roster agent: it is invisible to the coordinator's `list_agents` tool, it cannot be messaged with `send_to_agent`, and only the session's primary thread can consult it. Roster agents cannot. Advisor threads are exempt from the concurrent-thread limit. They appear in the session's [thread list](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration#threads) with `agent` set to the advisor form exactly as configured (`{"type": "advisor", "model": ...}`) and `parent_thread_id` set to the primary thread. Prompt caching on the advisor's side is automatic; there is nothing to configure. Consultations are billed at the advisor model's rates, and their tokens appear in the advisor thread's usage and in the session's usage totals. #### Removing the advisor To remove the advisor, [update the agent](https://platform.claude.com/docs/en/managed-agents/agent-setup#update-an-agent) with a roster that no longer includes the advisor entry. If the advisor is the roster's only entry, clear the roster entirely by setting `"multiagent": null`. ## Create the session Create a session referencing the coordinator. The coordinator delegates to the agents in its roster as needed. ```bash cURL session=$(curl -fsSL https://api.anthropic.com/v1/sessions \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d @- <beta->sessions->create( agent: $coordinator->id, environmentID: $environment->id, ); ``` ```ruby Ruby session = client.beta.sessions.create( agent: coordinator.id, environment_id: environment.id ) ``` ## Connect agents to MCP servers MCP servers are agent-scoped (each agent definition declares its own servers and tools), while vault credentials are session-scoped (`vault_ids` passed at session creation apply to every thread). Two implications for your integration: * To authenticate MCP servers, include a vault credential for every MCP server used across all agents. * To limit an agent's access, declare only the servers it needs in its agent definition. [Agent configuration overrides](https://platform.claude.com/docs/en/managed-agents/sessions#override-agent-configuration-for-a-session) at session creation can replace the coordinator's MCP servers and those of its `self` copies. ```bash cURL research_agent_id=$(curl --fail-with-body -sS "$BASE/v1/agents" "${H[@]}" --data @- <<'EOF' | jq -er '.id' { "name": "researcher", "model": "claude-haiku-4-5", "mcp_servers": [{"type": "url", "name": "github", "url": "https://api.githubcopilot.com/mcp/"}], "tools": [{"type": "mcp_toolset", "mcp_server_name": "github"}] } EOF ) coordinator_id=$(curl --fail-with-body -sS "$BASE/v1/agents" "${H[@]}" --data @- <beta->agents->create( name: 'researcher', model: 'claude-haiku-4-5', mcpServers: [ ['type' => 'url', 'name' => 'github', 'url' => 'https://api.githubcopilot.com/mcp/'], ], tools: [ ['type' => 'mcp_toolset', 'mcp_server_name' => 'github'], ], ); $coordinator = $client->beta->agents->create( name: 'coordinator', model: 'claude-opus-5', tools: [ ['type' => 'agent_toolset_20260401'], ], multiagent: [ 'type' => 'coordinator', 'agents' => [ ['type' => 'agent', 'id' => $researchAgent->id], ], ], ); $session = $client->beta->sessions->create( agent: $coordinator->id, environmentID: $environment->id, vaultIDs: [$vault->id], ); echo "{$session->id}\n"; ``` ```ruby Ruby research_agent = client.beta.agents.create( name: "researcher", model: "claude-haiku-4-5", mcp_servers: [ {type: "url", name: "github", url: "https://api.githubcopilot.com/mcp/"} ], tools: [ {type: "mcp_toolset", mcp_server_name: "github"} ] ) coordinator = client.beta.agents.create( name: "coordinator", model: "claude-opus-5", tools: [ {type: "agent_toolset_20260401"} ], multiagent: { type: "coordinator", agents: [ {type: "agent", id: research_agent.id} ] } ) session = client.beta.sessions.create( agent: coordinator.id, environment_id: environment.id, vault_ids: [vault.id] ) puts session.id ``` In this example, only the researcher declares the GitHub MCP server, so the coordinator does not have access. The session's `vault_ids` supply the GitHub credential to the researcher's thread. If an agent's MCP calls fail to authenticate after you declare the server, confirm the credential's `mcp_server_url` refers to the same server as the agent's `mcp_servers[].url`. Both URLs are normalized before matching (scheme and host lowercased, default ports and trailing slashes stripped), so differences in host casing, a default port, or a trailing slash don't prevent a match; a different path, subdomain, or non-default port does. ## Threads The **session-level event stream** (`/v1/sessions/{session_id}/events/stream`) is considered the **primary thread**, containing a condensed view of all activity across all threads. You don't see the full activity from subagents, but you do see the start and end of their work, and blocking events such as tool permission requests. **Session threads** are where you drill into a specific agent's activity. The session `status` is an aggregation of all agent activity; if at least one thread is `running`, then the overall session status is `running` as well. A [session budget](https://platform.claude.com/docs/en/managed-agents/budgets) is a single shared cap across all of a session's threads. As the cap is reached, threads pause independently, and each thread's cost is priced at the thread's own served model. A maximum of 25 concurrent threads is supported. The coordinator can call multiple copies of a single agent in the roster, creating multiple threads associated with one `agent`. [Advisor](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration#give-the-session-an-advisor) consultation threads are exempt from this limit. List all threads associated with a session as follows: ```bash cURL curl -fsS "https://api.anthropic.com/v1/sessions/$SESSION_ID/threads" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ | jq -r '.data[] | "[\(.agent.name)] \(.status)"' ``` ```bash CLI ant beta:sessions:threads list --session-id "$SESSION_ID" ``` ```python Python for thread in client.beta.sessions.threads.list(session.id): print(f"[{thread.agent.name}] {thread.status}") ``` ```typescript TypeScript for await (const thread of client.beta.sessions.threads.list(session.id)) { console.log(`[${thread.agent.name}] ${thread.status}`); } ``` ```csharp C# await foreach (var thread in (await client.Beta.Sessions.Threads.List(session.ID)).Paginate()) { Console.WriteLine($"[{thread.Agent.Name}] {thread.Status}"); } ``` ```go Go threads := client.Beta.Sessions.Threads.ListAutoPaging(ctx, session.ID, anthropic.BetaSessionThreadListParams{}) for threads.Next() { thread := threads.Current() fmt.Printf("[%s] %s\n", thread.Agent.Name, thread.Status) } if err := threads.Err(); err != nil { panic(err) } ``` ```java Java for (var thread : client.beta().sessions().threads().list(session.id()).autoPager()) { var name = thread.agent().isAgent() ? thread.agent().asAgent().name() : "advisor"; IO.println("[" + name + "] " + thread.status()); } ``` ```php PHP foreach ($client->beta->sessions->threads->list($session->id)->pagingEachItem() as $thread) { echo "[{$thread->agent->name}] {$thread->status}\n"; } ``` ```ruby Ruby client.beta.sessions.threads.list(session.id).auto_paging_each do |thread| puts "[#{thread.agent.name}] #{thread.status}" end ``` The full list includes the primary thread. `parent_thread_id` is null for the primary thread. Send `user.interrupt` with `session_thread_id` to stop a specific thread. Omitting `session_thread_id` interrupts every non-archived thread in the session, including the primary. ```bash cURL curl -fsS "https://api.anthropic.com/v1/sessions/$SESSION_ID/events?beta=true" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d "{\"events\": [{\"type\": \"user.interrupt\", \"session_thread_id\": \"$THREAD_ID\"}]}" ``` ```bash CLI ant beta:sessions:events send \ --session-id "$SESSION_ID" \ --event "{type: user.interrupt, session_thread_id: $THREAD_ID}" ``` ```python Python client.beta.sessions.events.send( session.id, events=[{"type": "user.interrupt", "session_thread_id": thread.id}], ) ``` ```typescript TypeScript await client.beta.sessions.events.send(session.id, { events: [{ type: "user.interrupt", session_thread_id: thread.id }], }); ``` ```csharp C# await client.Beta.Sessions.Events.Send(session.ID, new() { Events = [ new BetaManagedAgentsUserInterruptEventParams { Type = BetaManagedAgentsUserInterruptEventParamsType.UserInterrupt, SessionThreadID = thread.ID, }, ], }); ``` ```go Go if _, err := client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{ Events: []anthropic.BetaManagedAgentsEventParamsUnion{{ OfUserInterrupt: &anthropic.BetaManagedAgentsUserInterruptEventParams{ Type: anthropic.BetaManagedAgentsUserInterruptEventParamsTypeUserInterrupt, SessionThreadID: anthropic.String(thread.ID), }, }}, }); err != nil { panic(err) } ``` ```java Java client.beta().sessions().events().send( session.id(), EventSendParams.builder() .addEvent(BetaManagedAgentsUserInterruptEventParams.builder() .type(BetaManagedAgentsUserInterruptEventParams.Type.USER_INTERRUPT) .sessionThreadId(thread.id()) .build()) .build()); ``` ```php PHP $client->beta->sessions->events->send( $session->id, events: [ ['type' => 'user.interrupt', 'session_thread_id' => $thread->id], ], ); ``` ```ruby Ruby client.beta.sessions.events.send_( session.id, events: [{type: "user.interrupt", session_thread_id: thread.id}] ) ``` Against a child thread blocked on `requires_action`, the interrupt closes each pending tool call with an error tool result ("Tool execution was interrupted before completion. Please retry.") and re-emits `session.thread_status_idle` with `stop_reason: end_turn` directly; the model is not sampled. Against a thread already at `idle`, the interrupt is a no-op. Optionally archive a session thread when it has completed its work. This frees up a thread against the 25-thread limit. ```bash cURL curl -fsS -X POST "https://api.anthropic.com/v1/sessions/$SESSION_ID/threads/$THREAD_ID/archive" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" ``` ```bash CLI ant beta:sessions:threads archive \ --session-id "$SESSION_ID" \ --thread-id "$THREAD_ID" ``` ```python Python archived = client.beta.sessions.threads.archive(thread.id, session_id=session.id) print(archived.status, archived.archived_at) ``` ```typescript TypeScript const archived = await client.beta.sessions.threads.archive(thread.id, { session_id: session.id, }); console.log(archived.status, archived.archived_at); ``` ```csharp C# var archived = await client.Beta.Sessions.Threads.Archive(thread.ID, new() { SessionID = session.ID }); Console.WriteLine($"{archived.Status} {archived.ArchivedAt}"); ``` ```go Go archived, err := client.Beta.Sessions.Threads.Archive(ctx, thread.ID, anthropic.BetaSessionThreadArchiveParams{ SessionID: session.ID, }) if err != nil { panic(err) } fmt.Println(archived.Status, archived.ArchivedAt) ``` ```java Java var archived = client.beta().sessions().threads().archive( thread.id(), ThreadArchiveParams.builder() .sessionId(session.id()) .build()); IO.println(archived.status() + " " + archived.archivedAt().orElseThrow()); ``` ```php PHP $archived = $client->beta->sessions->threads->archive($thread->id, sessionID: $session->id); echo "{$archived->status} {$archived->archivedAt->format(DATE_ATOM)}\n"; ``` ```ruby Ruby archived = client.beta.sessions.threads.archive(thread.id, session_id: session.id) puts "#{archived.status} #{archived.archived_at}" ``` Archive only succeeds if the thread is `idle`. A thread parked on `requires_action` counts as idle and can be archived directly; only a running thread must be interrupted first: ```bash cURL # Interrupt the thread, then archive it curl -fsS "https://api.anthropic.com/v1/sessions/$SESSION_ID/events?beta=true" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d "{\"events\": [{\"type\": \"user.interrupt\", \"session_thread_id\": \"$THREAD_ID\"}]}" curl -fsS -X POST "https://api.anthropic.com/v1/sessions/$SESSION_ID/threads/$THREAD_ID/archive" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" ``` ```bash CLI ant beta:sessions:events send \ --session-id "$SESSION_ID" \ --event "{type: user.interrupt, session_thread_id: $THREAD_ID}" ant beta:sessions:threads archive \ --session-id "$SESSION_ID" \ --thread-id "$THREAD_ID" ``` ```python Python client.beta.sessions.events.send( session.id, events=[{"type": "user.interrupt", "session_thread_id": thread.id}], ) archived = client.beta.sessions.threads.archive(thread.id, session_id=session.id) print(archived.status, archived.archived_at) ``` ```typescript TypeScript await client.beta.sessions.events.send(session.id, { events: [{ type: "user.interrupt", session_thread_id: thread.id }], }); const archived = await client.beta.sessions.threads.archive(thread.id, { session_id: session.id, }); console.log(archived.status, archived.archived_at); ``` ```csharp C# await client.Beta.Sessions.Events.Send(session.ID, new() { Events = [ new BetaManagedAgentsUserInterruptEventParams { Type = BetaManagedAgentsUserInterruptEventParamsType.UserInterrupt, SessionThreadID = thread.ID, }, ], }); archived = await client.Beta.Sessions.Threads.Archive(thread.ID, new() { SessionID = session.ID }); Console.WriteLine($"{archived.Status} {archived.ArchivedAt}"); ``` ```go Go if _, err := client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{ Events: []anthropic.BetaManagedAgentsEventParamsUnion{{ OfUserInterrupt: &anthropic.BetaManagedAgentsUserInterruptEventParams{ Type: anthropic.BetaManagedAgentsUserInterruptEventParamsTypeUserInterrupt, SessionThreadID: anthropic.String(thread.ID), }, }}, }); err != nil { panic(err) } archived, err := client.Beta.Sessions.Threads.Archive(ctx, thread.ID, anthropic.BetaSessionThreadArchiveParams{ SessionID: session.ID, }) if err != nil { panic(err) } fmt.Println(archived.Status, archived.ArchivedAt) ``` ```java Java client.beta().sessions().events().send( session.id(), EventSendParams.builder() .addEvent(BetaManagedAgentsUserInterruptEventParams.builder() .type(BetaManagedAgentsUserInterruptEventParams.Type.USER_INTERRUPT) .sessionThreadId(thread.id()) .build()) .build()); archived = client.beta().sessions().threads().archive( thread.id(), ThreadArchiveParams.builder() .sessionId(session.id()) .build()); IO.println(archived.status() + " " + archived.archivedAt().orElseThrow()); ``` ```php PHP $client->beta->sessions->events->send( $session->id, events: [['type' => 'user.interrupt', 'session_thread_id' => $thread->id]], ); $archived = $client->beta->sessions->threads->archive($thread->id, sessionID: $session->id); echo "{$archived->status} {$archived->archivedAt->format(DATE_ATOM)}\n"; ``` ```ruby Ruby client.beta.sessions.events.send_( session.id, events: [{type: "user.interrupt", session_thread_id: thread.id}] ) archived = client.beta.sessions.threads.archive(thread.id, session_id: session.id) puts "#{archived.status} #{archived.archived_at}" ``` ### Primary thread events These events surface multiagent activity on the primary thread at `/v1/sessions/{session_id}/events/stream`. Message-direction events are named relative to the thread whose stream they appear on: `agent.thread_message_received` means a message arrived on this thread from another thread, and `agent.thread_message_sent` means this thread sent one. The task the coordinator delegates, for example, arrives on the child's own stream as an `agent.thread_message_received` event. | Type | Description | | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | `session.thread_created` | A thread was created. Includes `session_thread_id` and `agent_name`. | | `session.thread_status_running` | A thread started activity. | | `session.thread_status_idle` | The agent associated with the thread is awaiting input. Includes a `stop_reason` indicating why the agent stopped. | | `session.thread_status_terminated` | A thread was archived or encountered a terminal error. | | `agent.thread_message_received` | On the primary thread, an agent sent a report or question to the coordinator. Includes `from_session_thread_id`, `from_agent_name`, and `content`. | | `agent.thread_message_sent` | On the primary thread, the coordinator sent a task or follow-up message to another agent. Includes `to_session_thread_id`, `to_agent_name`, and `content`. | Advisor consultations emit these same thread events under the reserved name `anthropic.advisor` (as `agent_name` on the thread lifecycle events and `from_agent_name` on the advice delivery); see [Give the session an advisor](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration#give-the-session-an-advisor) for the sequence. ### Session thread events Critical events are proxied to the primary thread. However, you might still want to investigate a specific agent's reasoning and tool calls. To do so, stream or list the events from the associated session thread. Each session thread has its own event stream at `/v1/sessions/{session_id}/threads/{thread_id}/stream`, and it accepts the same `event_deltas[]` parameter as the session-level stream, so you can preview a subagent's text as the model generates it. A connection previews only the thread it's reading: a child thread's previews never appear on the session-level stream, so to watch a subagent live, open its own thread stream. See [Preview session thread events](https://platform.claude.com/docs/en/managed-agents/events-and-streaming#preview-session-thread-events) for opting in, accumulating, and reconciling previews. ```bash cURL curl -fsSN "https://api.anthropic.com/v1/sessions/$SESSION_ID/threads/$THREAD_ID/stream?beta=true" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" | while IFS= read -r line; do [[ $line == data:* ]] || continue json=${line#data: } case $(jq -r '.type' <<<"$json") in agent.message) printf '%s' "$(jq -j '.content[] | select(.type == "text") | .text' <<<"$json")" ;; session.thread_status_idle) break ;; esac done ``` ```bash CLI ant beta:sessions:threads:events stream \ --session-id "$SESSION_ID" \ --thread-id "$THREAD_ID" ``` ```python Python with client.beta.sessions.threads.events.stream( thread.id, session_id=session.id, ) as stream: for event in stream: match event.type: case "agent.message": for block in event.content: if block.type == "text": print(block.text, end="") case "session.thread_status_idle": break ``` ```typescript TypeScript const stream = await client.beta.sessions.threads.events.stream(thread.id, { session_id: session.id, }); for await (const event of stream) { if (event.type === "agent.message") { for (const block of event.content) { if (block.type === "text") { process.stdout.write(block.text); } } } else if (event.type === "session.thread_status_idle") { break; } } ``` ```csharp C# await foreach (var evt in client.Beta.Sessions.Threads.Events.StreamStreaming(thread.ID, new() { SessionID = session.ID })) { if (evt.Value is BetaManagedAgentsAgentMessageEvent message) { foreach (var block in message.Content) { if (block.Type == "text") { Console.Write(block.Text); } } } else if (evt.Value is BetaManagedAgentsSessionThreadStatusIdleEvent) { break; } } ``` ```go Go stream := client.Beta.Sessions.Threads.Events.StreamEvents(ctx, thread.ID, anthropic.BetaSessionThreadEventStreamParams{ SessionID: session.ID, }) defer stream.Close() loop: for stream.Next() { event := stream.Current() switch event.Type { case "agent.message": for _, block := range event.AsAgentMessage().Content { if block.Type == "text" { fmt.Print(block.Text) } } case "session.thread_status_idle": break loop } } if err := stream.Err(); err != nil { panic(err) } ``` ```java Java try (var streamResponse = client.beta().sessions().threads().events().streamStreaming( thread.id(), EventStreamParams.builder().sessionId(session.id()).build() )) { for (var event : (Iterable) streamResponse.stream()::iterator) { if (event.isAgentMessage()) { for (var block : event.asAgentMessage().content()) { block.text().ifPresent(textBlock -> IO.print(textBlock.text())); } } else if (event.isSessionThreadStatusIdle()) { break; } } } ``` ```php PHP $stream = $client->beta->sessions->threads->events->streamStream( $thread->id, sessionID: $session->id, ); foreach ($stream as $event) { if ($event->type === 'agent.message') { foreach ($event->content as $block) { if ($block->type === 'text') { echo $block->text; } } } elseif ($event->type === 'session.thread_status_idle') { break; } } ``` ```ruby Ruby client.beta.sessions.threads.events.stream_events(thread.id, session_id: session.id).each do |event| case event.type when :"agent.message" event.content.each do |block| print block.text if block.type == :text end when :"session.thread_status_idle" break end end ``` List all past session thread events to pull a complete history. ```bash cURL curl -fsS "https://api.anthropic.com/v1/sessions/$SESSION_ID/threads/$THREAD_ID/events" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ | jq -r '.data[] | "[\(.type)] \(.processed_at)"' ``` ```bash CLI ant beta:sessions:threads:events list \ --session-id "$SESSION_ID" \ --thread-id "$THREAD_ID" ``` ```python Python for event in client.beta.sessions.threads.events.list( thread.id, session_id=session.id, ): print(f"[{event.type}] {event.processed_at}") ``` ```typescript TypeScript for await (const event of client.beta.sessions.threads.events.list(thread.id, { session_id: session.id, })) { console.log(`[${event.type}] ${event.processed_at}`); } ``` ```csharp C# var page = await client.Beta.Sessions.Threads.Events.List(thread.ID, new() { SessionID = session.ID }); await foreach (var evt in page.Paginate()) { Console.WriteLine($"[{evt.Type}] {evt.ProcessedAt}"); } ``` ```go Go pager := client.Beta.Sessions.Threads.Events.ListAutoPaging(ctx, thread.ID, anthropic.BetaSessionThreadEventListParams{ SessionID: session.ID, }) for pager.Next() { event := pager.Current() fmt.Printf("[%s] %s\n", event.Type, event.ProcessedAt) } if err := pager.Err(); err != nil { panic(err) } ``` ```java Java for (var event : client.beta().sessions().threads().events().list( thread.id(), EventListParams.builder().sessionId(session.id()).build() ).autoPager()) { var type = event._json().orElseThrow() instanceof JsonObject json ? json.values().get("type").asStringOrThrow() : "unknown"; var processedAt = event.processedAt().map(OffsetDateTime::toString).orElse("pending"); IO.println("[" + type + "] " + processedAt); } ``` ```php PHP foreach ( $client->beta->sessions->threads->events->list( $thread->id, sessionID: $session->id, )->pagingEachItem() as $event ) { echo "[{$event->type}] {$event->processedAt->format(DATE_RFC3339)}\n"; } ``` ```ruby Ruby client.beta.sessions.threads.events.list( thread.id, session_id: session.id ).auto_paging_each do |event| puts "[#{event.type}] #{event.processed_at}" end ``` ### Tool permissions and custom tools If a subagent needs something from your client, such as [permission](https://platform.claude.com/docs/en/managed-agents/events-and-streaming#tool-confirmation) to run an `always_ask` tool, or the [result of a custom tool](https://platform.claude.com/docs/en/managed-agents/events-and-streaming#handling-custom-tool-calls), the event is cross-posted to the **primary thread** with `session_thread_id` identifying the originating session thread. ```json { "type": "session.thread_status_idle", "id": "sevt_01ABC...", "session_thread_id": "sth_01DEF...", "agent_name": "code-reviewer", "stop_reason": { "type": "requires_action", "event_ids": ["sevt_01XYZ..."] } } ``` Post `user.tool_confirmation` (with `tool_use_id`) or `user.custom_tool_result` (with `custom_tool_use_id`); the server routes the response to the correct thread automatically. The following example extends the [tool confirmation handler](https://platform.claude.com/docs/en/managed-agents/events-and-streaming#tool-confirmation) to route replies. The same pattern applies to `user.custom_tool_result`. ```bash cURL while IFS= read -r event_id; do jq -n --arg id "$event_id" \ '{events: [{type: "user.tool_confirmation", tool_use_id: $id, result: "allow"}]}' | curl -fsS "https://api.anthropic.com/v1/sessions/$SESSION_ID/events?beta=true" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d @- done < <(jq -r '.stop_reason.event_ids[]' <<<"$data") ``` ```bash CLI # This workflow does not translate well to a one-off shell command. # Use one of the SDK examples in this code group instead. ``` ```python Python for event_id in stop.event_ids: client.beta.sessions.events.send( session.id, events=[ { "type": "user.tool_confirmation", "tool_use_id": event_id, "result": "allow", } ], ) ``` ```typescript TypeScript for (const eventId of stop.event_ids) { await client.beta.sessions.events.send(session.id, { events: [ { type: "user.tool_confirmation", tool_use_id: eventId, result: "allow", }, ], }); } ``` ```csharp C# foreach (var eventId in requiresAction.EventIds) { await client.Beta.Sessions.Events.Send(session.ID, new() { Events = [ new BetaManagedAgentsUserToolConfirmationEventParams { Type = BetaManagedAgentsUserToolConfirmationEventParamsType.UserToolConfirmation, ToolUseID = eventId, Result = BetaManagedAgentsUserToolConfirmationEventParamsResult.Allow, }, ], }); } ``` ```go Go for _, eventID := range stopReason.EventIDs { params := anthropic.BetaManagedAgentsUserToolConfirmationEventParams{ Type: anthropic.BetaManagedAgentsUserToolConfirmationEventParamsTypeUserToolConfirmation, ToolUseID: eventID, Result: anthropic.BetaManagedAgentsUserToolConfirmationEventParamsResultAllow, } if _, err := client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{ Events: []anthropic.BetaManagedAgentsEventParamsUnion{{OfUserToolConfirmation: ¶ms}}, }); err != nil { panic(err) } } ``` ```java Java for (var eventId : pendingToolUseIds) { client.beta().sessions().events().send( session.id(), EventSendParams.builder() .addEvent(BetaManagedAgentsUserToolConfirmationEventParams.builder() .toolUseId(eventId) .result(BetaManagedAgentsUserToolConfirmationEventParams.Result.ALLOW) .build()) .build() ); } ``` ```php PHP foreach ($event->stopReason->eventIDs as $eventId) { $client->beta->sessions->events->send($session->id, events: [[ 'type' => 'user.tool_confirmation', 'tool_use_id' => $eventId, 'result' => 'allow', ]]); } ``` ```ruby Ruby event_ids.each do |event_id| client.beta.sessions.events.send_(session.id, events: [{ type: "user.tool_confirmation", tool_use_id: event_id, result: "allow" }]) end ``` --- title: Scheduled deployments url: https://platform.claude.com/docs/en/managed-agents/scheduled-deployments description: "Create and manage deployments with the Claude API: run an agent on a recurring cron schedule and inspect its run history." --- A **scheduled deployment** allows an [agent](https://platform.claude.com/docs/en/managed-agents/agent-setup) to start [sessions](https://platform.claude.com/docs/en/managed-agents/sessions) autonomously, enabling task completion over a predictable cadence. You create and manage deployments with the Deployments API, part of the Claude API. For the launch context and examples of what teams run on schedules, see [scheduled deployments and vaults in Claude Managed Agents](https://claude.com/blog/whats-new-in-claude-managed-agents) on the blog. All Managed Agents API requests require the `managed-agents-2026-04-01` beta header. The SDK sets the beta header automatically. ## Create a scheduled deployment When creating a deployment, you pass the [session configurations](https://platform.claude.com/docs/en/managed-agents/sessions) required for execution, in addition to a `schedule`. * Deployments require [agent configuration](https://platform.claude.com/docs/en/managed-agents/agent-setup) and [environment configuration](https://platform.claude.com/docs/en/managed-agents/environments), and optionally accept [files](https://platform.claude.com/docs/en/managed-agents/files), [GitHub](https://platform.claude.com/docs/en/managed-agents/github), [memory stores](https://platform.claude.com/docs/en/managed-agents/memory), and [vaults](https://platform.claude.com/docs/en/managed-agents/vaults). * Deployments also require at least one initial event, a `user.message` or `user.define_outcome`, that starts each session's work. * In the `schedule`, you define a cron `expression` and a `timezone`. Maximum granularity supported is at the minute level. ```bash cURL DEPLOYMENT_ID=$( curl --fail-with-body -sS "https://api.anthropic.com/v1/deployments?beta=true" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d @- <beta->deployments->create( name: 'Weekly compliance scan', agent: $agent->id, environmentID: $environment->id, initialEvents: [ [ 'type' => 'user.message', 'content' => [['type' => 'text', 'text' => 'Run the weekly compliance scan.']], ], ], schedule: [ 'type' => 'cron', 'expression' => '0 20 * * 5', 'timezone' => 'America/New_York', ], ); ``` ```ruby Ruby deployment = client.beta.deployments.create( name: "Weekly compliance scan", agent: agent.id, environment_id: environment.id, initial_events: [ { type: "user.message", content: [{type: "text", text: "Run the weekly compliance scan."}] } ], schedule: { type: "cron", expression: "0 20 * * 5", timezone: "America/New_York" } ) ``` The response includes a deployment object with a populated `schedule.upcoming_runs_at` with the next upcoming fire times, to confirm your schedule was set correctly. ```json { "id": "depl_01xyz", "status": "active", "paused_reason": null, "schedule": { "type": "cron", "expression": "0 20 * * 5", "timezone": "America/New_York", "last_run_at": null, "upcoming_runs_at": [ "2026-05-09T00:00:00Z", "2026-05-16T00:00:00Z", "2026-05-23T00:00:00Z" ] } } ``` The upcoming run timestamps reflect the exact schedule configured. However, to distribute load, actual execution applies jitter of up to 15% of the interval between runs, with a minimum of 5 seconds and a maximum of 9 minutes. A maximum of **1,000 scheduled deployments** is supported per organization. Contact Anthropic support if you need more. See the [Create Deployment reference](https://platform.claude.com/docs/en/api/beta/deployments/create) for full parameters and response schema. ### Cron and timezone semantics * **Expression:** Standard POSIX cron (`minute hour day-of-month month day-of-week`). You can generate and validate these cron expressions in the [Claude Console](https://platform.claude.com/workspaces/default/deployments). * **Timezone:** IANA timezone identifier (for example, `"America/Los_Angeles"`). * **DST:** Cron schedules use literal wall-clock matching, so `"0 20 * * *"` in `America/New_York` fires at 8PM local time regardless of whether EST or EDT is in effect. Wall-clock times that do not exist on a spring-forward day (such as 2 AM) are not triggered. Wall-clock times that occur twice on a fall-back day fire twice. Schedule outside the 1–3 AM local window, or use UTC, when missed or duplicate executions are unacceptable. ### Set a budget on each run Pass the optional `budget` object when you create or update the deployment. It takes the same shape as a [session budget](https://platform.claude.com/docs/en/managed-agents/budgets). The deployment copies the cap onto each session it starts, so the budget bounds every run separately rather than acting as a cumulative ceiling across runs: a deployment with a `"2000"` cap can spend up to about $20 on every run. A session started by the deployment behaves exactly like any other budgeted session: it pauses with `budget_reached` when its own list cost [reaches the cap](https://platform.claude.com/docs/en/managed-agents/budgets#when-a-session-reaches-its-budget). Changing the deployment's budget applies to runs started afterward; a session already running keeps the cap it started with, which you can [change through the session itself](https://platform.claude.com/docs/en/managed-agents/session-operations#updating-the-session-budget). Unlike a session budget, a deployment's budget can be removed with `"budget": null` and set again later. The following example sets a budget on an existing deployment: ```bash cURL curl --fail-with-body -sS "https://api.anthropic.com/v1/deployments/$DEPLOYMENT_ID?beta=true" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d @- <<'EOF' { "budget": { "type": "limit", "max_list_cost": {"amount": "2000", "currency": "USD"} } } EOF ``` ## Deployment runs Deployments can fail to trigger for a variety of reasons: for example, if the `environment` resource has been archived, or if session creation is rate-limited. Each attempt at executing a deployment generates a **deployment run** record, allowing you to track successes and failures independent of the session lifecycle. Successful deployments generate active sessions, and a successful deployment run contains the associated `session_id`. To follow a session's lifecycle, track the session events through the [event stream](https://platform.claude.com/docs/en/managed-agents/events-and-streaming) or [webhooks](https://platform.claude.com/docs/en/managed-agents/webhooks). Deployment lifecycle changes and the outcome of each scheduled run are also delivered as webhook events, listed in the Deployment events and Deployment run events tabs of [Supported event types](https://platform.claude.com/docs/en/managed-agents/webhooks#supported-event-types). List all deployment runs for a deployment as follows: ```bash cURL curl --fail-with-body -sS "https://api.anthropic.com/v1/deployment_runs?beta=true&deployment_id=$DEPLOYMENT_ID" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" ``` ```bash CLI ant beta:deployment-runs list --deployment-id "$DEPLOYMENT_ID" ``` ```python Python for run in client.beta.deployment_runs.list( deployment_id=deployment.id, ): print(run.created_at, run.session_id or run.error.type) ``` ```typescript TypeScript for await (const run of client.beta.deploymentRuns.list({ deployment_id: deployment.id, })) { console.log(run.created_at, run.session_id ?? run.error?.type); } ``` ```csharp C# var runs = await client.Beta.DeploymentRuns.List( new() { DeploymentID = deployment.ID } ); await foreach (var run in runs.Paginate()) { // The Error union exposes .Message directly; the discriminator is read // from .Json until a common .Type accessor is added. var outcome = run.SessionID ?? run.Error!.Json.GetProperty("type").GetString(); Console.WriteLine($"{run.CreatedAt} {outcome}"); } ``` ```go Go runs := client.Beta.DeploymentRuns.ListAutoPaging(ctx, anthropic.BetaDeploymentRunListParams{ DeploymentID: anthropic.String(deployment.ID), }) for runs.Next() { run := runs.Current() if run.SessionID != "" { fmt.Println(run.CreatedAt.Format(time.RFC3339), run.SessionID) } else { fmt.Println(run.CreatedAt.Format(time.RFC3339), run.Error.Type) } } if err := runs.Err(); err != nil { panic(err) } ``` ```java Java for (var run : client.beta().deploymentRuns().list( DeploymentRunListParams.builder() .deploymentId(deployment.id()) .build()).autoPager()) { // The Error union does not yet expose common .type()/.message() // accessors; .toString() includes both. IO.println(run.createdAt() + " " + run.sessionId().orElseGet(() -> run.error().orElseThrow().toString())); } ``` ```php PHP foreach ($client->beta->deploymentRuns->list( deploymentID: $deployment->id, )->pagingEachItem() as $run) { $outcome = $run->sessionID ?? $run->error->type; echo "{$run->createdAt->format(DATE_ATOM)} {$outcome}\n"; } ``` ```ruby Ruby client.beta.deployment_runs.list( deployment_id: deployment.id ).auto_paging_each do puts "#{it.created_at} #{it.session_id || it.error.type}" end ``` You can additionally filter on deployment runs with errors: ```bash cURL curl --fail-with-body -sS "https://api.anthropic.com/v1/deployment_runs?beta=true&deployment_id=$DEPLOYMENT_ID&has_error=true" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" ``` ```bash CLI ant beta:deployment-runs list --deployment-id "$DEPLOYMENT_ID" --has-error ``` ```python Python for run in client.beta.deployment_runs.list( deployment_id=deployment.id, has_error=True, ): print(run.created_at, run.error.type, run.error.message) ``` ```typescript TypeScript for await (const run of client.beta.deploymentRuns.list({ deployment_id: deployment.id, has_error: true, })) { console.log(run.created_at, run.error?.type, run.error?.message); } ``` ```csharp C# var failedRuns = await client.Beta.DeploymentRuns.List( new() { DeploymentID = deployment.ID, HasError = true } ); await foreach (var failedRun in failedRuns.Paginate()) { var error = failedRun.Error!; var errorType = error.Json.GetProperty("type").GetString(); Console.WriteLine($"{failedRun.CreatedAt} {errorType} {error.Message}"); } ``` ```go Go failedRuns := client.Beta.DeploymentRuns.ListAutoPaging(ctx, anthropic.BetaDeploymentRunListParams{ DeploymentID: anthropic.String(deployment.ID), HasError: anthropic.Bool(true), }) for failedRuns.Next() { failedRun := failedRuns.Current() fmt.Println(failedRun.CreatedAt.Format(time.RFC3339), failedRun.Error.Type, failedRun.Error.Message) } if err := failedRuns.Err(); err != nil { panic(err) } ``` ```java Java for (var run : client.beta().deploymentRuns().list( DeploymentRunListParams.builder() .deploymentId(deployment.id()) .hasError(true) .build()).autoPager()) { IO.println(run.createdAt() + " " + run.error().orElseThrow()); } ``` ```php PHP foreach ($client->beta->deploymentRuns->list( deploymentID: $deployment->id, hasError: true, )->pagingEachItem() as $run) { echo "{$run->createdAt->format(DATE_ATOM)} {$run->error->type} {$run->error->message}\n"; } ``` ```ruby Ruby client.beta.deployment_runs.list( deployment_id: deployment.id, has_error: true ).auto_paging_each do puts "#{it.created_at} #{it.error.type} #{it.error.message}" end ``` A failed run includes an `error` with a `type` describing why session creation was rejected (for example, `environment_archived_error`, `agent_archived_error`, or `session_rate_limited_error`). See the [List Deployment Runs reference](https://platform.claude.com/docs/en/api/beta/deployment_runs/list) for all filter parameters and the response schema. ```json { "type": "deployment_run", "id": "drun_01abc124", "deployment_id": "depl_01xyz", "trigger_context": { "type": "schedule", "scheduled_at": "2026-05-09T00:00:00Z" }, "session_id": null, "error": { "type": "environment_archived_error", "message": "environment `env_01abc` is archived" }, "agent": { "type": "agent", "id": "agent_01ghi789", "version": 3 }, "created_at": "2026-05-09T00:00:01Z" } ``` To retrieve a single run by ID, call [`GET /v1/deployment_runs/{deployment_run_id}`](https://platform.claude.com/docs/en/api/beta/deployment_runs/retrieve). A [`deployment_run` webhook event](https://platform.claude.com/docs/en/managed-agents/webhooks#supported-event-types) carries the run ID as its `data.id`. ## Managing deployment lifecycle Each lifecycle change emits a [webhook event](https://platform.claude.com/docs/en/managed-agents/webhooks#supported-event-types), so you can react to a paused, unpaused, or archived deployment without polling; see the Deployment events tab. **Pause** suppresses scheduled triggers on a go-forward basis; running sessions from a prior deployment run continue to execute. Manual runs through the `run` endpoint are still allowed while paused. Pausing sets `paused_reason` to `{"type": "manual"}`; unpausing clears it. ```bash cURL curl --fail-with-body -sS -X POST "https://api.anthropic.com/v1/deployments/$DEPLOYMENT_ID/pause?beta=true" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" ``` ```bash CLI ant beta:deployments pause --deployment-id "$DEPLOYMENT_ID" ``` ```python Python client.beta.deployments.pause(deployment.id) ``` ```typescript TypeScript await client.beta.deployments.pause(deployment.id); ``` ```csharp C# await client.Beta.Deployments.Pause(deployment.ID); ``` ```go Go if _, err := client.Beta.Deployments.Pause(ctx, deployment.ID, anthropic.BetaDeploymentPauseParams{}); err != nil { panic(err) } ``` ```java Java client.beta().deployments().pause(deployment.id()); ``` ```php PHP $client->beta->deployments->pause($deployment->id); ``` ```ruby Ruby client.beta.deployments.pause(deployment.id) ``` **Unpause** resumes the schedule from the next scheduled occurrence. Missed triggers are not backfilled. ```bash cURL curl --fail-with-body -sS -X POST "https://api.anthropic.com/v1/deployments/$DEPLOYMENT_ID/unpause?beta=true" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" ``` ```bash CLI ant beta:deployments unpause --deployment-id "$DEPLOYMENT_ID" ``` ```python Python client.beta.deployments.unpause(deployment.id) ``` ```typescript TypeScript await client.beta.deployments.unpause(deployment.id); ``` ```csharp C# await client.Beta.Deployments.Unpause(deployment.ID); ``` ```go Go if _, err := client.Beta.Deployments.Unpause(ctx, deployment.ID, anthropic.BetaDeploymentUnpauseParams{}); err != nil { panic(err) } ``` ```java Java client.beta().deployments().unpause(deployment.id()); ``` ```php PHP $client->beta->deployments->unpause($deployment->id); ``` ```ruby Ruby client.beta.deployments.unpause(deployment.id) ``` **Archive**, unlike **pause**, is terminal: the schedule terminates and the deployment cannot be modified. ```bash cURL curl --fail-with-body -sS -X POST "https://api.anthropic.com/v1/deployments/$DEPLOYMENT_ID/archive?beta=true" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" ``` ```bash CLI ant beta:deployments archive --deployment-id "$DEPLOYMENT_ID" ``` ```python Python client.beta.deployments.archive(deployment.id) ``` ```typescript TypeScript await client.beta.deployments.archive(deployment.id); ``` ```csharp C# await client.Beta.Deployments.Archive(deployment.ID); ``` ```go Go if _, err := client.Beta.Deployments.Archive(ctx, deployment.ID, anthropic.BetaDeploymentArchiveParams{}); err != nil { panic(err) } ``` ```java Java client.beta().deployments().archive(deployment.id()); ``` ```php PHP $client->beta->deployments->archive($deployment->id); ``` ```ruby Ruby client.beta.deployments.archive(deployment.id) ``` ### Failure behavior Session creation rate-limit responses are recorded immediately as a `session_rate_limited_error` run without retry; the schedule attempts again at the next scheduled occurrence. Rate limits on underlying API calls within a session are handled by the session itself. If a deployment's agent has been archived, the deployment is automatically archived in the same operation. If the agent has been deleted, the next scheduled trigger detects the missing agent and automatically archives the deployment. In both cases no deployment run is recorded. If a subagent referenced by the agent has been archived, the next trigger records a failed run with `error.type: "agent_archived_error"` and the deployment is automatically paused so you can update the agent and resume. Other unrecoverable session-creation errors, such as an archived environment or vault, behave the same way: the trigger records a failed run and the deployment is automatically paused. The deployment's `paused_reason.error.type` mirrors the failed run's `error.type`. ## Trigger a manual run To run a deployment outside its schedule, call the [`run` endpoint](https://platform.claude.com/docs/en/api/beta/deployments/run). This creates a session immediately and writes a deployment run with `trigger_context.type: "manual"`. This allows you to test a deployment before committing to the schedule. ```bash cURL curl --fail-with-body -sS -X POST "https://api.anthropic.com/v1/deployments/$DEPLOYMENT_ID/run?beta=true" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" ``` ```bash CLI ant beta:deployments run --deployment-id "$DEPLOYMENT_ID" ``` ```python Python run = client.beta.deployments.run(deployment.id) ``` ```typescript TypeScript const run = await client.beta.deployments.run(deployment.id); ``` ```csharp C# var manualRun = await client.Beta.Deployments.Run(deployment.ID); ``` ```go Go manualRun, err := client.Beta.Deployments.Run(ctx, deployment.ID, anthropic.BetaDeploymentRunParams{}) if err != nil { panic(err) } ``` ```java Java var run = client.beta().deployments().run(deployment.id()); ``` ```php PHP $run = $client->beta->deployments->run($deployment->id); ``` ```ruby Ruby run = client.beta.deployments.run(deployment.id) ``` ### Reference --- title: Reference url: https://platform.claude.com/docs/en/managed-agents/reference description: Event types, self-hosted worker CLI flags, supported MCP server types, rate limits, and branding guidelines for Claude Managed Agents. --- This page collects reference material for Claude Managed Agents. For task-oriented guides, follow the links in each section. For the operations on the session resource, see [Session operations](https://platform.claude.com/docs/en/managed-agents/session-operations). Managed Agents API requests require the `managed-agents-2026-04-01` beta header, except memory store endpoints, which use `agent-memory-2026-07-22` instead. The SDK sets the correct beta header automatically. See [Beta headers](https://platform.claude.com/docs/en/api/beta-headers#endpoint-specific-headers). ## Event types Persisted event type strings follow a `{domain}.{action}` naming convention; the stream-only event deltas (see the Event deltas tab) are the exception. See [Session event stream](https://platform.claude.com/docs/en/managed-agents/events-and-streaming) for sending, streaming, and listing events. | Type | Description | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `user.message` | A user message with text, image, or document content. | | `user.interrupt` | Stop the agent mid-execution. | | `user.custom_tool_result` | Response to a custom tool call from the agent. | | `user.tool_confirmation` | Approve or deny an agent or MCP tool call when a permission policy requires confirmation. | | `user.define_outcome` | Define an [outcome](https://platform.claude.com/docs/en/managed-agents/define-outcomes) for the agent to work toward. | | `user.tool_result` | For sessions with `self_hosted` [environments](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes) only, your integration is responsible for providing `agent_toolset` results. The SDK helpers and CLI do this automatically. | | Type | Description | | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `agent.message` | Agent response content blocks. | | `agent.thinking` | Signals the agent is making forward progress through extended thinking. This is a progress signal only and does not carry the thinking content. | | `agent.tool_use` | Agent invokes a pre-built agent tool (bash, file operations, and so on). | | `agent.tool_result` | Result of a pre-built agent tool execution. | | `agent.mcp_tool_use` | Agent invokes an MCP server tool. | | `agent.mcp_tool_result` | Result of an MCP tool execution. | | `agent.custom_tool_use` | Agent invokes one of your custom tools. Respond with a `user.custom_tool_result` event. | | `agent.thread_context_compacted` | Conversation history was compacted to fit the context window. | | `agent.thread_message_received` | In a [multiagent](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration) session, a message from another thread arrived on the thread whose stream carries this event; on the primary thread, an agent sent a report or question to the coordinator. | | `agent.thread_message_sent` | In a [multiagent](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration) session, the thread whose stream carries this event sent a message to another thread; on the primary thread, the coordinator sent a task or follow-up message to another agent. | Message content in these events can include a `redacted` content block, `{"type": "redacted"}`: a placeholder for content withheld by Anthropic model policy. The block carries no other fields. Redacted blocks appear only in content the platform emits; a user event that includes one is rejected with a 400 error. | Type | Description | | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `session.status_running` | Agent is actively processing. | | `session.status_idle` | Agent finished its current task and is waiting for input. Includes a `stop_reason` indicating why the agent stopped. | | `session.status_rescheduled` | A transient error occurred and the session is retrying automatically. | | `session.status_terminated` | Session ended, either because of an unrecoverable error or because it was archived. | | `session.deleted` | Session was deleted. Terminates any active event stream; no further events are emitted for this session. | | `session.updated` | Session update request changed at least one field. Includes only the fields that changed. Updates apply on the next turn. | | `session.error` | An error occurred during processing. Includes a typed `error` object with a `retry_status`. | | `session.usage` | Snapshot of the session's cumulative usage and tracked list cost. Carries the session's usage totals and an echo of the session's [budget](https://platform.claude.com/docs/en/managed-agents/budgets), or `null` when the session has none. | | `session.thread_created` | A [multiagent](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration) thread was created. | | `session.thread_status_running` | A session thread began executing. Every session emits this for its primary thread; in [multiagent](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration) sessions, child-thread transitions are also cross-posted to the primary stream. | | `session.thread_status_idle` | A session thread finished its turn and is awaiting input. Includes `stop_reason`. | | `session.thread_status_rescheduled` | A session thread hit a transient error and is retrying automatically. | | `session.thread_status_terminated` | A session thread was archived or reached a terminal error. | Span events are observability markers that wrap activity for timing and usage tracking. | Type | Description | | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `span.model_request_start` | A model inference call has started. | | `span.model_request_end` | A model inference call has completed. Includes `model_usage` with token counts. | | `span.outcome_evaluation_start` | [Outcome](https://platform.claude.com/docs/en/managed-agents/define-outcomes) evaluation has started. | | `span.outcome_evaluation_ongoing` | Heartbeat during an ongoing [outcome](https://platform.claude.com/docs/en/managed-agents/define-outcomes) evaluation. | | `span.outcome_evaluation_end` | An [outcome](https://platform.claude.com/docs/en/managed-agents/define-outcomes) evaluation cycle has completed. A `needs_revision` result means another cycle follows; `satisfied`, `max_iterations_reached`, `failed`, and `interrupted` are terminal. | | Type | Description | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `system.message` | Append privileged system-level context that applies to the accompanying turn and all subsequent turns. Supported on Claude Opus 4.8, Claude Fable 5, Claude Mythos 5, and Claude Opus 5; on an unsupported primary model the event is rejected with `model_does_not_support_mid_conversation_system`. | Event deltas are stream-only preview events. They are emitted on stream connections (session-level or per-thread) that opt in with the `event_deltas[]` parameter, and they are never persisted to the session's event history. See [Event deltas](https://platform.claude.com/docs/en/managed-agents/events-and-streaming#event-deltas) for opting in, accumulating, and reconciling them. | Type | Description | | ------------- | ------------------------------------------------------------------------------------------------------------------------ | | `event_start` | A previewed event has started generating. Carries the upcoming event's `type` and `id`. Stream-only and never persisted. | | `event_delta` | Incremental content for a previewed event, identified by `event_id`. Stream-only and never persisted. | ## Self-hosted worker These are the `ant beta:worker` CLI flags for the pre-built worker that drives a `self_hosted` environment. See [Self-hosted sandboxes](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes) for setting up the environment, running a worker, and the SDK helper options. | Flag | Description | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--environment-id` | The environment to poll for work. Also reads from `ANTHROPIC_ENVIRONMENT_ID`. | | `--environment-key` | Authenticates the worker with this environment. Also reads from `ANTHROPIC_ENVIRONMENT_KEY`. | | `--workdir` | Directory where skills are downloaded and tools read and write files. Defaults to `.` (the current directory); the system default working directory is `/workspace`. | | `--on-work` | Script to call for each claimed work item instead of running tools in-process. Receives session details as environment variables. | | `--unrestricted-paths` | Allow the file tools to read and write paths outside `--workdir`. The workdir check is a guardrail for the file tools only, not a sandbox; it does not constrain bash. | | `--max-idle` | How long to wait after the session goes idle with an `end_turn` [stop reason](https://platform.claude.com/docs/en/api/handling-stop-reasons) before shutting down. Defaults to `60s`. | | `--log-format` | Log output format. Use `json` for structured log ingestion. Defaults to `text`. | ## Supported MCP server types Claude Managed Agents connects to [remote MCP servers](https://platform.claude.com/docs/en/agents-and-tools/remote-mcp-servers) that expose an HTTP endpoint, or to private MCP servers through [MCP tunnels](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/overview). The server should support the MCP protocol's streamable HTTP transport; servers that only support the deprecated SSE transport still work through an automatic fallback. See [MCP connector](https://platform.claude.com/docs/en/managed-agents/mcp-connector) for declaring servers on an agent. For more information on MCP and building MCP servers, see the [MCP documentation](https://modelcontextprotocol.io). ## Rate limits Managed Agents endpoints are rate-limited per organization: | Operation | Limit | | ------------------------------------------------------------- | ------------------------- | | Create endpoints (such as agents, sessions, and environments) | 300 requests per minute | | Read endpoints (such as retrieve, list, and stream) | 1,200 requests per minute | Organization-level [spend limits and usage-tier rate limits](https://platform.claude.com/docs/en/api/rate-limits) also apply. ## Branding guidelines For partners integrating Claude Managed Agents, use of Claude branding is optional. When referencing Claude in your product: **Allowed:** * "Claude Agent" (preferred for dropdown menus) * "Claude" (when within a menu already labeled "Agents") * "\{YourAgentName} Powered by Claude" (if you have an existing agent name) **Not permitted:** * "Claude Code" or "Claude Code Agent" * "Claude Cowork" or "Claude Cowork Agent" * Claude Code-branded ASCII art or visual elements that mimic Claude Code Your product should maintain its own branding and not appear to be Claude Code, Claude Cowork, or any other Anthropic product. For questions about branding compliance, contact the Anthropic [sales team](https://www.anthropic.com/contact-sales). ## Admin ### Organization --- title: Admin API url: https://platform.claude.com/docs/en/manage-claude/admin-api description: Manage organization members, workspaces, invites, and API keys programmatically with the Admin API, using an Admin API key or an `org:admin` OAuth token. --- **The Admin API is unavailable for individual accounts.** To collaborate with teammates and add members, set up your organization in **Console → Settings → Organization**. The [Admin API](https://platform.claude.com/docs/en/api/admin) allows you to programmatically manage your organization's resources, including organization members, workspaces, and API keys. This provides programmatic control over administrative tasks that would otherwise require manual configuration in the [Claude Console](https://platform.claude.com/). **The Admin API requires special access** The Admin API accepts two credentials: * An **Admin API key** (starting with `sk-ant-admin...`) sent in the `x-api-key` header. Only organization members with the admin role can provision one. See [Create an Admin API key](https://platform.claude.com/docs/en/manage-claude/admin-api-keys). * An **OAuth bearer token** with the `org:admin` scope sent in the `authorization: Bearer` header. Only members with the admin, owner, or primary owner role can obtain one. See [Obtain an OAuth bearer token](https://platform.claude.com/docs/en/manage-claude/admin-api#oauth-bearer-token). **Claude Enterprise:** Claude Enterprise (claude.ai) organizations use the Admin API too, with a scoped API key created in claude.ai. Of the endpoints on this page, only members and invites are available to them (in beta), alongside Claude-Enterprise-only endpoints: groups and custom-role reads (beta), and [spend limits](https://platform.claude.com/docs/en/manage-claude/spend-limits-api). See [User management](https://platform.claude.com/docs/en/manage-claude/user-management) for Claude Enterprise. **Claude Platform on AWS:** Most of the Admin API is not available on Claude Platform on AWS. Workspace endpoints (create, get, list, update, and archive on `/v1/organizations/workspaces`) are available. Other endpoints including organization members, workspace members, invites, API keys, usage reports, cost reports, and rate limit reports are not available. See [Claude Platform on AWS](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws) for details. ## Authentication Authenticate with either credential. An Admin API key covers most endpoints; the service-account, federation-issuer, and federation-rule endpoints accept only an `org:admin` OAuth token. The following examples call the [organization info endpoint](https://platform.claude.com/docs/en/manage-claude/admin-api#accessing-organization-info) both ways. ### OAuth bearer token Log in with the [`ant` CLI](https://platform.claude.com/docs/en/cli-sdks-libraries/cli/quickstart) under a dedicated profile, requesting the `org:admin` scope (see [Admin access](https://platform.claude.com/docs/en/cli-sdks-libraries/cli/authentication#admin-access)), then export the bearer token. A dedicated profile keeps your routine commands from running with elevated access: ```bash CLI ant auth login --profile admin --scope "org:admin" export ANTHROPIC_OAUTH_TOKEN=$(ant auth print-credentials --profile admin --access-token) ``` Interactive tokens are short-lived; if requests start returning 401, re-run the `export` command, which refreshes the token automatically. Call the Admin API with the exported token: ```bash cURL curl --fail-with-body -sS "https://api.anthropic.com/v1/organizations/me" \ --header "anthropic-version: 2023-06-01" \ --header "authorization: Bearer $ANTHROPIC_OAUTH_TOKEN" ``` An `org:admin` token grants access to the whole organization, regardless of the workspace the underlying profile or [federation rule](https://platform.claude.com/docs/en/manage-claude/admin-api#federation-rules) is bound to. For CI and other non-interactive workloads, mint the token with Workload Identity Federation instead of logging in interactively. See [Manage WIF with the Admin API](https://platform.claude.com/docs/en/manage-claude/wif-admin-api#workload-ci-and-automation). ### Admin API key To create an Admin API key for your organization type, see [Create an Admin API key](https://platform.claude.com/docs/en/manage-claude/admin-api-keys). ```bash cURL curl --fail-with-body -sS "https://api.anthropic.com/v1/organizations/me" \ --header "anthropic-version: 2023-06-01" \ --header "x-api-key: $ANTHROPIC_ADMIN_KEY" ``` ## How the Admin API works When you use the Admin API: 1. You make requests using either credential from the [Authentication](https://platform.claude.com/docs/en/manage-claude/admin-api#authentication) section 2. The API allows you to manage: * Organization members and their roles * Organization member invites * Workspaces and their members * API keys * Service accounts, federation issuers, and federation rules (these endpoints require an `org:admin` OAuth token; Admin API keys are not accepted) This is useful for: * Automating user onboarding/offboarding * Programmatically managing workspace access * Monitoring and managing API key usage ## Organization roles and permissions There are five organization-level roles. See more details in the [API Console roles and permissions](https://support.claude.com/en/articles/10186004-api-console-roles-and-permissions) article. | Role | Permissions | | ------------------ | ----------------------------------------------------------------------------- | | user | Can use Workbench | | claude\_code\_user | Can use Workbench and [Claude Code](https://code.claude.com/docs/en/overview) | | developer | Can use Workbench and manage API keys | | billing | Can use Workbench and manage billing details | | admin | Can do all of the preceding, plus manage users | Organization owners and primary owners have all admin permissions and can additionally manage admins. All references to the admin role on this page also apply to owners and primary owners. ## Key concepts ### Organization members You can list [organization members](https://platform.claude.com/docs/en/api/admin-api/users/get-user), update member roles, and remove members. ```bash cURL # List organization members curl "https://api.anthropic.com/v1/organizations/users?limit=10" \ --header "anthropic-version: 2023-06-01" \ --header "x-api-key: $ANTHROPIC_ADMIN_KEY" # Update member role curl "https://api.anthropic.com/v1/organizations/users/{user_id}" \ --header "anthropic-version: 2023-06-01" \ --header "content-type: application/json" \ --header "x-api-key: $ANTHROPIC_ADMIN_KEY" \ --data '{"role": "developer"}' # Remove member curl --request DELETE "https://api.anthropic.com/v1/organizations/users/{user_id}" \ --header "anthropic-version: 2023-06-01" \ --header "x-api-key: $ANTHROPIC_ADMIN_KEY" ``` ### Organization invites You can invite users to organizations and manage those [invites](https://platform.claude.com/docs/en/api/admin-api/invites/get-invite). ```bash cURL # Create invite curl --request POST "https://api.anthropic.com/v1/organizations/invites" \ --header "anthropic-version: 2023-06-01" \ --header "content-type: application/json" \ --header "x-api-key: $ANTHROPIC_ADMIN_KEY" \ --data '{ "email": "newuser@domain.com", "role": "developer" }' # List invites curl "https://api.anthropic.com/v1/organizations/invites?limit=10" \ --header "anthropic-version: 2023-06-01" \ --header "x-api-key: $ANTHROPIC_ADMIN_KEY" # Delete invite curl --request DELETE "https://api.anthropic.com/v1/organizations/invites/{invite_id}" \ --header "anthropic-version: 2023-06-01" \ --header "x-api-key: $ANTHROPIC_ADMIN_KEY" ``` ### Workspaces For a comprehensive guide to workspaces, including Console and API examples, see [Workspaces](https://platform.claude.com/docs/en/manage-claude/workspaces). ### Workspace members Manage [user access to specific workspaces](https://platform.claude.com/docs/en/api/admin-api/workspace_members/get-workspace-member): ```bash cURL # Add member to workspace curl --request POST "https://api.anthropic.com/v1/organizations/workspaces/{workspace_id}/members" \ --header "anthropic-version: 2023-06-01" \ --header "content-type: application/json" \ --header "x-api-key: $ANTHROPIC_ADMIN_KEY" \ --data '{ "user_id": "user_xxx", "workspace_role": "workspace_developer" }' # List workspace members curl "https://api.anthropic.com/v1/organizations/workspaces/{workspace_id}/members?limit=10" \ --header "anthropic-version: 2023-06-01" \ --header "x-api-key: $ANTHROPIC_ADMIN_KEY" # Update member role curl --request POST "https://api.anthropic.com/v1/organizations/workspaces/{workspace_id}/members/{user_id}" \ --header "anthropic-version: 2023-06-01" \ --header "content-type: application/json" \ --header "x-api-key: $ANTHROPIC_ADMIN_KEY" \ --data '{ "workspace_role": "workspace_admin" }' # Remove member from workspace curl --request DELETE "https://api.anthropic.com/v1/organizations/workspaces/{workspace_id}/members/{user_id}" \ --header "anthropic-version: 2023-06-01" \ --header "x-api-key: $ANTHROPIC_ADMIN_KEY" ``` ### API keys Monitor and manage [API keys](https://platform.claude.com/docs/en/api/admin/api_keys/list). Each key in the response includes its `expires_at` timestamp (`null` for keys without an [expiration](https://platform.claude.com/docs/en/manage-claude/authentication#key-expiration)): ```bash cURL # List API keys curl "https://api.anthropic.com/v1/organizations/api_keys?limit=10&status=active&workspace_id=wrkspc_xxx" \ --header "anthropic-version: 2023-06-01" \ --header "x-api-key: $ANTHROPIC_ADMIN_KEY" # Update API key curl --request POST "https://api.anthropic.com/v1/organizations/api_keys/{api_key_id}" \ --header "anthropic-version: 2023-06-01" \ --header "content-type: application/json" \ --header "x-api-key: $ANTHROPIC_ADMIN_KEY" \ --data '{ "status": "inactive", "name": "New Key Name" }' ``` ### Service accounts Create and manage service accounts (`svac_...`), the non-human identities that [Workload Identity Federation](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation) tokens act as. Admin API keys are not accepted on the service-account, federation-issuer, or federation-rule endpoints; use an `org:admin` OAuth token. See [Manage WIF with the Admin API](https://platform.claude.com/docs/en/manage-claude/wif-admin-api#service-accounts). ### Federation issuers Register the OIDC identity providers (`fdis_...`) whose tokens may assert workload identity for your organization. See [Manage WIF with the Admin API](https://platform.claude.com/docs/en/manage-claude/wif-admin-api#federation-issuers). ### Federation rules Manage the rules (`fdrl_...`) that map issuer tokens to service accounts and scopes. See [Manage WIF with the Admin API](https://platform.claude.com/docs/en/manage-claude/wif-admin-api#federation-rules). ## Accessing organization info Get information about your organization programmatically with the `/v1/organizations/me` endpoint. For example: ```bash cURL curl "https://api.anthropic.com/v1/organizations/me" \ --header "anthropic-version: 2023-06-01" \ --header "x-api-key: $ANTHROPIC_ADMIN_KEY" ``` ```json { "id": "12345678-1234-5678-1234-567812345678", "type": "organization", "name": "Organization Name" } ``` This endpoint is useful for programmatically determining which organization an Admin API key belongs to. For complete parameter details and response schemas, see the [Organization Info API reference](https://platform.claude.com/docs/en/api/admin-api/organization/get-me). ## Usage and cost reports Track your organization's usage and costs with the [Usage and Cost API](https://platform.claude.com/docs/en/manage-claude/usage-cost-api). ## Claude Code analytics Monitor developer productivity and Claude Code adoption with the [Claude Code Analytics API](https://platform.claude.com/docs/en/manage-claude/claude-code-analytics-api). ## Rate limits Read the rate limits configured for your organization and its workspaces with the [Rate Limits API](https://platform.claude.com/docs/en/manage-claude/rate-limits-api). ## Compliance API Retrieve audit and activity data for your organization with the [Compliance API](https://platform.claude.com/docs/en/manage-claude/compliance-api). Admin API keys can read the Activity Feed only; for full access, see [Set up the Compliance API](https://platform.claude.com/docs/en/manage-claude/compliance-api-access). ## Best practices To effectively use the Admin API: * Use meaningful names and descriptions for workspaces and API keys * Implement proper error handling for failed operations * Regularly audit member roles and permissions * Clean up unused workspaces and expired invites * Monitor API key usage, audit each key's [`expires_at`](https://platform.claude.com/docs/en/manage-claude/authentication#key-expiration), and rotate keys periodically ## FAQ The Admin API accepts either an Admin API key (starting with `sk-ant-admin`) or an OAuth bearer token with the `org:admin` scope. Only organization members with the admin role can provision Admin API keys, and only members with the admin, owner, or primary owner role can obtain `org:admin` tokens. See [Authentication](https://platform.claude.com/docs/en/manage-claude/admin-api#authentication). No, new API keys can only be created through the Claude Console for security reasons. The Admin API can only manage existing API keys. API keys persist in their current state as they are scoped to the organization, not to individual users. No, organization members with the admin role cannot be removed through the API for security reasons. Organization invites expire after 21 days. There is currently no way to modify this expiration period. For workspace-specific questions, see the [Workspaces FAQ](https://platform.claude.com/docs/en/manage-claude/workspaces#faq). --- title: User management url: https://platform.claude.com/docs/en/manage-claude/user-management description: "Manage the people in your Claude Enterprise organization with the Admin API: list members and change roles, send and withdraw invites, manage groups, and read custom roles." --- This page covers managing the people in your **Claude Enterprise** (claude.ai) organization programmatically, using the [Admin API](https://platform.claude.com/docs/en/api/admin): list members and look them up by email address, change a member's role, remove members, send and withdraw invites, manage your enterprise's groups and their membership, and read your organization's custom roles. For Claude Console (Claude Platform) organizations, see the [Admin API guide for Claude Console](https://platform.claude.com/docs/en/manage-claude/admin-api). **The endpoints on this page are in beta for Claude Enterprise organizations.** The beta is enabled for all Claude Enterprise organizations. Group and custom-role requests must include the [beta header](https://platform.claude.com/docs/en/api/beta-headers) `anthropic-beta: ce-user-management-2026-07-13`; requests without it return 404. Member and invite requests take no beta header. ## Which endpoints can your organization use? The Admin API is a single set of endpoints under `https://api.anthropic.com/v1/organizations/`. Claude Console and Claude Enterprise organizations authenticate with [different keys](https://platform.claude.com/docs/en/manage-claude/admin-api-keys) and each have access to a different subset of the endpoints: | Endpoints | Claude Console (Claude Platform) | Claude Enterprise (claude.ai) | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------- | | [Members](https://platform.claude.com/docs/en/manage-claude/user-management#members) and [invites](https://platform.claude.com/docs/en/manage-claude/user-management#invites) | Available; see the [Admin API guide](https://platform.claude.com/docs/en/manage-claude/admin-api) | **Beta** (this page) | | [Groups](https://platform.claude.com/docs/en/manage-claude/user-management#groups) | Not available | **Beta** (this page) | | [Custom roles](https://platform.claude.com/docs/en/manage-claude/user-management#custom-roles) | Not available | **Beta**, read-only (this page) | | [Spend limits](https://platform.claude.com/docs/en/manage-claude/spend-limits-api) | Not available | Available | | [Workspaces](https://platform.claude.com/docs/en/manage-claude/workspaces), [API keys](https://platform.claude.com/docs/en/manage-claude/admin-api#api-keys), [usage and cost reports](https://platform.claude.com/docs/en/manage-claude/usage-cost-api), [rate limits](https://platform.claude.com/docs/en/manage-claude/rate-limits-api), and the other endpoints in the [Admin API guide](https://platform.claude.com/docs/en/manage-claude/admin-api) | Available | Not available | Members and invites are the same endpoints for both organization types; this page documents their Claude Enterprise behavior, including the Claude Enterprise [organization roles](https://platform.claude.com/docs/en/manage-claude/user-management#organization-roles). The group and custom-role endpoints exist only for Claude Enterprise. **Scoped Admin API key required** These endpoints require an Admin API key with the `read:members` scope (member and invite `GET` endpoints, and all custom-role endpoints; there is no separate role scope), the `write:members` scope (member and invite `POST` and `DELETE` endpoints), the `read:rbac_groups` scope (group `GET` endpoints), or the `write:rbac_groups` scope (group `POST` and `DELETE` endpoints). A key carrying the `read:org_audit` scope (a read-only scope for security-audit integrations) can also call every `GET` endpoint on this page and the [Compliance API](https://platform.claude.com/docs/en/manage-claude/compliance-api) read endpoints. See [Create an Admin API key](https://platform.claude.com/docs/en/manage-claude/admin-api-keys#create-a-key-for-a-claude-enterprise-organization) for where your primary owner creates one and which scopes to select. Pass the key in the `x-api-key` header on every request. Member and invite requests also require the `anthropic-version: 2023-06-01` header, as shown in the examples; group and custom-role requests do not, and instead require the `anthropic-beta` header described in the preceding note. ## Overview This page covers five resources: | Resource | Endpoints | Use for | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | | **Members** | `GET /v1/organizations/users` `GET /v1/organizations/users/{user_id}` `POST /v1/organizations/users/{user_id}` `DELETE /v1/organizations/users/{user_id}` | List the organization's members or look one up by email; change a member's role; remove a member. | | **Invites** | `POST /v1/organizations/invites` `GET /v1/organizations/invites` `GET /v1/organizations/invites/{invite_id}` `DELETE /v1/organizations/invites/{invite_id}` | Invite a person to the organization, track the invitation's status, and withdraw it before it is accepted. | | **Groups** | `GET /v1/organizations/rbac_groups` `GET /v1/organizations/rbac_groups/{group_id}` `POST /v1/organizations/rbac_groups` `POST /v1/organizations/rbac_groups/{group_id}` `DELETE /v1/organizations/rbac_groups/{group_id}` | Read your enterprise's groups and the custom roles attached to each; create, rename, and delete groups. | | **Group members** | `GET /v1/organizations/rbac_groups/{group_id}/members` `POST /v1/organizations/rbac_groups/{group_id}/members` `DELETE /v1/organizations/rbac_groups/{group_id}/members/{user_id}` | Read a group's members; add and remove members. | | **Custom roles** | `GET /v1/organizations/rbac_roles` `GET /v1/organizations/rbac_roles/{role_id}` `GET /v1/organizations/rbac_roles/{role_id}/permissions` | Read your organization's custom roles and the permissions each role grants. | Custom roles and their group attachments are managed in [claude.ai organization settings](https://claude.ai/admin-settings); the API reads them but cannot change them. ## Quick start List the organization's members, newest first: ```bash cURL curl "https://api.anthropic.com/v1/organizations/users?limit=20" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" \ -H "anthropic-version: 2023-06-01" ``` ```json { "data": [ { "type": "user", "id": "user_01AbCdEfGhIjKlMnOpQrSt", "email": "jane@example.com", "name": "Jane Smith", "role": "user", "added_at": "2026-06-12T09:14:03Z" } ], "has_more": false, "first_id": "user_01AbCdEfGhIjKlMnOpQrSt", "last_id": "user_01AbCdEfGhIjKlMnOpQrSt" } ``` ## Key concepts ### Organization roles Every member has exactly one organization role. Reads return the member's role as one of five values: | Role | Meaning | | ------------------ | ----------------------------------------------------------------------------------------- | | `user` | A standard member. | | `managed` | A member whose permissions are granted through the custom roles attached to their groups. | | `owner` | An organization owner. | | `membership_admin` | A member who can manage the organization's members. | | `primary_owner` | The organization's primary owner. There is exactly one. | The API can assign only the `user` and `managed` roles, on invite creation and on role updates. The administrative roles (`owner`, `membership_admin`, and `primary_owner`) are assigned in claude.ai organization settings, and members holding them cannot be modified or removed through this API. ### Members and invites A person becomes a member by accepting an invite (or through your organization's single sign-on, where configured). Creating an invite sends an invitation email; the invite then reads as `pending` until the recipient accepts (`accepted`) or its server-assigned `expires_at` passes (`expired`). Only a `pending` invite can be withdrawn. To change a pending invitation's email address or role, withdraw it and create a new one. If your organization's plan draws members from a finite pool of purchased seats, a pending invite consumes a seat. The create-invite endpoint does not take a seat or tier parameter: the seat is assigned automatically from the lowest tier that has availability. Creating an invite when no seat is free fails with a 400 error rather than purchasing a seat. Withdrawing the invite, letting it expire, or removing the member later returns the seat to the pool. ### Groups and roles Groups connect members to custom roles (role-based access control, the `rbac` in the endpoint paths and scope names). Groups are owned by your enterprise as a whole (the parent organization together with every organization under it) rather than by a single organization, so the group scopes (`read:rbac_groups` and `write:rbac_groups`) require a key created for all linked organizations. Each group carries a `source_type`: `direct` for groups created in claude.ai, `scim` for groups provisioned by your identity provider. A group's `roles` field lists the IDs of the custom roles attached to it; resolve them to names and permissions with the [custom role endpoints](https://platform.claude.com/docs/en/manage-claude/user-management#custom-roles), noting that the role catalog is per-organization while groups are enterprise-wide, so fetching a role that belongs to a different organization of your enterprise returns 404 for your key. The field is `null` (rather than `[]`) when role data was temporarily unavailable, so retry to distinguish a degraded read from a group with no roles. ## Rate limits Admin API endpoints share a per-organization limit of **100 requests per minute**; invite creation has its own limit of **1,200 requests per hour** instead. Requests over a limit return **429 Too Many Requests**. ## Pagination Member and invite lists use ID-based pagination: pass `limit` (default 20, max 1000) plus at most one of `before_id` or `after_id`, and page using the `first_id` and `last_id` fields of each response until `has_more` is `false`. Group and custom-role lists use an **opaque cursor** instead: the response's `next_page` value is passed unchanged as the `page` parameter on the next request, until `next_page` is `null`. ## Error responses Error responses follow the standard shape documented in [Errors](https://platform.claude.com/docs/en/api/errors). ## Members ### List members `GET /v1/organizations/users` returns the organization's members, most recently added first. Filter by `email` to look up a specific member; the match is case-insensitive and tolerates common variants of the same address (for example, `jane+hiring@example.com` matches `jane@example.com`). Requires the `read:members` scope. For complete parameter details and response schemas, see [List users](https://platform.claude.com/docs/en/api/admin/users/list) in the API reference. ```bash cURL curl "https://api.anthropic.com/v1/organizations/users?email=jane@example.com" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" \ -H "anthropic-version: 2023-06-01" ``` ### Get a member `GET /v1/organizations/users/{user_id}` returns one member by ID. Requires the `read:members` scope. For complete parameter details and response schemas, see [Get user](https://platform.claude.com/docs/en/api/admin/users/retrieve) in the API reference. ```bash cURL curl "https://api.anthropic.com/v1/organizations/users/user_01AbCdEfGhIjKlMnOpQrSt" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" \ -H "anthropic-version: 2023-06-01" ``` ### Change a member's role `POST /v1/organizations/users/{user_id}` sets the member's role to `user` or `managed`. Members holding an administrative role (`owner`, `membership_admin`, or `primary_owner`) cannot be changed through this endpoint, and administrative roles cannot be assigned; both return 400 and are managed in claude.ai organization settings. If your organization's identity provider manages roles (advanced SSO or advanced SCIM provisioning), role updates return 400. Requires the `write:members` scope. For complete parameter details and response schemas, see [Update user](https://platform.claude.com/docs/en/api/admin/users/update) in the API reference. ```bash cURL curl -X POST "https://api.anthropic.com/v1/organizations/users/user_01AbCdEfGhIjKlMnOpQrSt" \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{"role": "managed"}' ``` ### Remove a member `DELETE /v1/organizations/users/{user_id}` removes the member from the organization, returning any purchased seat they occupied to the organization's pool. Members holding an administrative role cannot be removed through this endpoint, and if your identity provider manages membership (SCIM), removals return 400. Requires the `write:members` scope. For complete parameter details and response schemas, see [Remove user](https://platform.claude.com/docs/en/api/admin/users/delete) in the API reference. ```bash cURL curl -X DELETE "https://api.anthropic.com/v1/organizations/users/user_01AbCdEfGhIjKlMnOpQrSt" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" \ -H "anthropic-version: 2023-06-01" ``` ```json { "type": "user_deleted", "id": "user_01AbCdEfGhIjKlMnOpQrSt" } ``` ## Invites ### Create an invite `POST /v1/organizations/invites` sends an invitation email and returns the invite with a server-assigned `expires_at`. `role` must be `user` or `managed`. If a pending invite already exists for the email address, or the address already belongs to a member, the request returns 400 naming the existing resource. Organizations whose identity provider provisions users automatically (JIT or SCIM) cannot create invites through the API. Requires the `write:members` scope. On plans that draw members from a finite seat pool, the invite automatically takes a seat from the lowest tier that has availability; the API does not take a tier parameter. If no seat is free, the request fails with a 400 error rather than purchasing a seat. Add seats through the organization's plan management and retry. The optional `rbac_group_ids` field lists groups (by `rbac_group_`-prefixed ID) to assign to the member when they accept. Passing a non-empty `rbac_group_ids` additionally requires the key to carry the `write:rbac_groups` scope, because group assignment can grant the permissions attached to the group's roles. For complete parameter details and response schemas, see [Create invite](https://platform.claude.com/docs/en/api/admin/invites/create) in the API reference. ```bash cURL curl -X POST "https://api.anthropic.com/v1/organizations/invites" \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "email": "newhire@example.com", "role": "managed", "rbac_group_ids": ["rbac_group_01UvWxYzAbCdEfGhIjKlMn"] }' ``` ```json { "type": "invite", "id": "invite_01QrStUvWxYzAbCdEfGhIj", "email": "newhire@example.com", "role": "managed", "invited_at": "2026-07-06T16:20:11Z", "expires_at": "2026-07-27T16:20:11Z", "accepted_at": null, "status": "pending", "rbac_group_ids": ["rbac_group_01UvWxYzAbCdEfGhIjKlMn"] } ``` ### List invites `GET /v1/organizations/invites` returns the organization's invites, most recent first, across the `pending`, `accepted`, and `expired` states; there is no status filter. Requires the `read:members` scope. For complete parameter details and response schemas, see [List invites](https://platform.claude.com/docs/en/api/admin/invites/list) in the API reference. ```bash cURL curl "https://api.anthropic.com/v1/organizations/invites?limit=20" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" \ -H "anthropic-version: 2023-06-01" ``` ### Get an invite `GET /v1/organizations/invites/{invite_id}` returns one invite by ID. Requires the `read:members` scope. For complete parameter details and response schemas, see [Get invite](https://platform.claude.com/docs/en/api/admin/invites/retrieve) in the API reference. ```bash cURL curl "https://api.anthropic.com/v1/organizations/invites/invite_01QrStUvWxYzAbCdEfGhIj" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" \ -H "anthropic-version: 2023-06-01" ``` ### Withdraw an invite `DELETE /v1/organizations/invites/{invite_id}` withdraws a `pending` invite, deactivating the link in the invitation email. Withdrawing an `accepted` invite returns 400 (remove the member instead); withdrawing an `expired` invite returns 400. Requires the `write:members` scope. For complete parameter details and response schemas, see [Delete invite](https://platform.claude.com/docs/en/api/admin/invites/delete) in the API reference. ```bash cURL curl -X DELETE "https://api.anthropic.com/v1/organizations/invites/invite_01QrStUvWxYzAbCdEfGhIj" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" \ -H "anthropic-version: 2023-06-01" ``` ## Groups Groups your enterprise creates directly, in [claude.ai organization settings](https://claude.ai/admin-settings) or through this API (`source_type: "direct"`), support every endpoint in this section. Groups provisioned by your identity provider (`source_type: "scim"`) can be read but not modified: renaming or deleting a SCIM group, or changing its membership, returns 400, because your identity provider owns it. Every group request must include the `anthropic-beta: ce-user-management-2026-07-13` header, as shown in the examples; requests without it return 404. Unlike member and invite requests, group requests do not require the `anthropic-version` header. ### List groups `GET /v1/organizations/rbac_groups` returns your enterprise's groups, including identity-provider-managed (`scim`) groups. Requires the `read:rbac_groups` scope. For complete parameter details and response schemas, see [List groups](https://platform.claude.com/docs/en/api/admin/rbac_groups/list) in the API reference. ```bash cURL curl "https://api.anthropic.com/v1/organizations/rbac_groups?limit=20" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" \ -H "anthropic-beta: ce-user-management-2026-07-13" ``` ```json { "data": [ { "type": "rbac_group", "id": "rbac_group_01UvWxYzAbCdEfGhIjKlMn", "name": "Engineering", "source_type": "direct", "roles": ["rbac_role_01CdEfGhIjKlMnOpQrStUv"], "created_at": "2026-03-18T10:01:42Z", "updated_at": "2026-05-02T08:55:09Z" } ], "has_more": false, "next_page": null } ``` ### Get a group `GET /v1/organizations/rbac_groups/{group_id}` returns one group by ID. Requires the `read:rbac_groups` scope. For complete parameter details and response schemas, see [Get group](https://platform.claude.com/docs/en/api/admin/rbac_groups/retrieve) in the API reference. ```bash cURL curl "https://api.anthropic.com/v1/organizations/rbac_groups/rbac_group_01UvWxYzAbCdEfGhIjKlMn" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" \ -H "anthropic-beta: ce-user-management-2026-07-13" ``` ### Create a group `POST /v1/organizations/rbac_groups` creates a group with the given `name` (1–255 characters) and no roles or members. Requires the `write:rbac_groups` scope. For complete parameter details and response schemas, see [Create group](https://platform.claude.com/docs/en/api/admin/rbac_groups/create) in the API reference. ```bash cURL curl -X POST "https://api.anthropic.com/v1/organizations/rbac_groups" \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" \ -H "anthropic-beta: ce-user-management-2026-07-13" \ -d '{"name": "Engineering"}' ``` ```json { "type": "rbac_group", "id": "rbac_group_01UvWxYzAbCdEfGhIjKlMn", "name": "Engineering", "source_type": "direct", "roles": [], "created_at": "2026-07-09T18:00:00Z", "updated_at": "2026-07-09T18:00:00Z" } ``` ### Rename a group `POST /v1/organizations/rbac_groups/{group_id}` updates the group. `name` is the only field this endpoint can change. Requires the `write:rbac_groups` scope. For complete parameter details and response schemas, see [Update group](https://platform.claude.com/docs/en/api/admin/rbac_groups/update) in the API reference. ```bash cURL curl -X POST "https://api.anthropic.com/v1/organizations/rbac_groups/rbac_group_01UvWxYzAbCdEfGhIjKlMn" \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" \ -H "anthropic-beta: ce-user-management-2026-07-13" \ -d '{"name": "Platform Engineering"}' ``` ### Delete a group `DELETE /v1/organizations/rbac_groups/{group_id}` deletes the group. Its members remain members of their organizations, but they lose the permissions of its attached roles, and a group [spend limit](https://platform.claude.com/docs/en/manage-claude/spend-limits-api), if one existed, stops applying to them. Requires the `write:rbac_groups` scope. For complete parameter details and response schemas, see [Delete group](https://platform.claude.com/docs/en/api/admin/rbac_groups/delete) in the API reference. ```bash cURL curl -X DELETE "https://api.anthropic.com/v1/organizations/rbac_groups/rbac_group_01UvWxYzAbCdEfGhIjKlMn" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" \ -H "anthropic-beta: ce-user-management-2026-07-13" ``` ```json { "id": "rbac_group_01UvWxYzAbCdEfGhIjKlMn", "type": "rbac_group_deleted" } ``` ### List a group's members `GET /v1/organizations/rbac_groups/{group_id}/members` returns the group's members (each with their `user_id` and email), oldest first. Only current members of your enterprise's organizations are returned, so a page might contain fewer than `limit` entries while `has_more` is `true`. Requires the `read:rbac_groups` scope. For complete parameter details and response schemas, see [List group members](https://platform.claude.com/docs/en/api/admin/rbac_groups/members/list) in the API reference. ```bash cURL curl "https://api.anthropic.com/v1/organizations/rbac_groups/rbac_group_01UvWxYzAbCdEfGhIjKlMn/members?limit=100" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" \ -H "anthropic-beta: ce-user-management-2026-07-13" ``` ```json { "data": [ { "type": "rbac_group_member", "group_id": "rbac_group_01UvWxYzAbCdEfGhIjKlMn", "user_id": "user_01AbCdEfGhIjKlMnOpQrSt", "email": "jane@example.com", "created_at": "2026-04-07T12:30:00Z" } ], "has_more": false, "next_page": null } ``` ### Add a member to a group `POST /v1/organizations/rbac_groups/{group_id}/members` adds an organization member to the group by `user_id`. The user must already be a member of one of your enterprise's organizations (the request returns 404 otherwise), and adding someone who is already in the group returns 400. For `scim` groups, membership is managed in your identity provider and this request returns 400. To assign groups to a person who has not joined yet, use `rbac_group_ids` on [invite creation](https://platform.claude.com/docs/en/manage-claude/user-management#create-an-invite) instead. Requires the `write:rbac_groups` scope. For complete parameter details and response schemas, see [Add group member](https://platform.claude.com/docs/en/api/admin/rbac_groups/members/create) in the API reference. ```bash cURL curl -X POST "https://api.anthropic.com/v1/organizations/rbac_groups/rbac_group_01UvWxYzAbCdEfGhIjKlMn/members" \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" \ -H "anthropic-beta: ce-user-management-2026-07-13" \ -d '{"user_id": "user_01AbCdEfGhIjKlMnOpQrSt"}' ``` ```json { "type": "rbac_group_member", "group_id": "rbac_group_01UvWxYzAbCdEfGhIjKlMn", "user_id": "user_01AbCdEfGhIjKlMnOpQrSt", "email": "jane@example.com", "created_at": "2026-07-09T18:00:00Z" } ``` ### Remove a member from a group `DELETE /v1/organizations/rbac_groups/{group_id}/members/{user_id}` removes the member from the group; they remain a member of their organization. The request returns 404 if the user is not a member of the group, and 400 for `scim` groups, whose membership is managed in your identity provider. Requires the `write:rbac_groups` scope. For complete parameter details and response schemas, see [Remove group member](https://platform.claude.com/docs/en/api/admin/rbac_groups/members/delete) in the API reference. ```bash cURL curl -X DELETE "https://api.anthropic.com/v1/organizations/rbac_groups/rbac_group_01UvWxYzAbCdEfGhIjKlMn/members/user_01AbCdEfGhIjKlMnOpQrSt" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" \ -H "anthropic-beta: ce-user-management-2026-07-13" ``` ```json { "group_id": "rbac_group_01UvWxYzAbCdEfGhIjKlMn", "user_id": "user_01AbCdEfGhIjKlMnOpQrSt", "type": "rbac_group_member_deleted" } ``` ## Custom roles Custom roles are read-only through the API: these endpoints catalog your organization's custom roles (defined in [claude.ai organization settings](https://claude.ai/admin-settings) or provisioned by Anthropic) and the permissions each role grants. Custom-role reads use the `read:members` scope (there is no separate role scope) and work with an organization-level key: unlike the group endpoints, they do not require a key created for all linked organizations, and the catalog returned is your organization's own. Custom-role requests, like group requests, must include the `anthropic-beta: ce-user-management-2026-07-13` header; requests without it return 404. ### List roles `GET /v1/organizations/rbac_roles` returns your organization's custom roles. Requires the `read:members` scope. For complete parameter details and response schemas, see [List roles](https://platform.claude.com/docs/en/api/admin/rbac_roles/list) in the API reference. ```bash cURL curl "https://api.anthropic.com/v1/organizations/rbac_roles?limit=20" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" \ -H "anthropic-beta: ce-user-management-2026-07-13" ``` ```json { "data": [ { "type": "rbac_role", "id": "rbac_role_01CdEfGhIjKlMnOpQrStUv", "name": "Engineering base", "created_at": "2026-03-18T10:01:42Z", "updated_at": "2026-05-02T08:55:09Z" } ], "has_more": false, "next_page": null } ``` ### Get a role `GET /v1/organizations/rbac_roles/{role_id}` returns one role by ID. Requires the `read:members` scope. For complete parameter details and response schemas, see [Get role](https://platform.claude.com/docs/en/api/admin/rbac_roles/retrieve) in the API reference. ```bash cURL curl "https://api.anthropic.com/v1/organizations/rbac_roles/rbac_role_01CdEfGhIjKlMnOpQrStUv" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" \ -H "anthropic-beta: ce-user-management-2026-07-13" ``` ### List a role's permissions `GET /v1/organizations/rbac_roles/{role_id}/permissions` returns the role's permissions. Each permission pairs a `resource` (what it applies to: the organization's product features, a connector tool, a connector OAuth scope, one connector, or every connector) with an `action` (what it grants on that resource). Rows for features not enabled for your organization are omitted, so a page might contain fewer than `limit` rows while `has_more` is `true`. Requires the `read:members` scope. Two `action` values need special care: an `organization` permission whose action is `capability_access_all` (every product feature) or `capability_access_all_ga` (every generally available product feature) is a blanket grant (one that covers neither model access nor the `permission_`-prefixed admin-panel permissions) and is listed as that single row rather than expanded. When you tally what a role grants, treat a blanket row as covering everything its variant describes, not just the features named in other rows. For complete parameter details and response schemas, see [List role permissions](https://platform.claude.com/docs/en/api/admin/rbac_roles/permissions/list) in the API reference. ```bash cURL curl "https://api.anthropic.com/v1/organizations/rbac_roles/rbac_role_01CdEfGhIjKlMnOpQrStUv/permissions?limit=20" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" \ -H "anthropic-beta: ce-user-management-2026-07-13" ``` ```json { "data": [ { "type": "rbac_role_permission", "resource": { "type": "organization", "organization_id": "12345678-1234-5678-1234-567812345678" }, "action": "capability_access_all_ga" }, { "type": "rbac_role_permission", "resource": { "type": "connector_tool", "connector_id": "mcpsrv_01WxYzAbCdEfGhIjKlMnOp", "tool_name": "search_tickets" }, "action": "use" } ], "has_more": false, "next_page": null } ``` ## Example workflows ### Offboard a departing employee 1. Look up the member by email: ```bash cURL curl "https://api.anthropic.com/v1/organizations/users?email=departing@example.com" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" \ -H "anthropic-version: 2023-06-01" ``` 2. Remove them with `DELETE /v1/organizations/users/{user_id}`, using the `id` from the response. Their seat, if any, returns to the pool. 3. If the person had not yet joined, the lookup returns no member; list invites and withdraw their `pending` invite instead. ### Audit group membership 1. List groups and record each group's `id`, `name`, and `roles`. 2. For each group that carries sensitive roles, page through `GET /v1/organizations/rbac_groups/{group_id}/members` and compare the member emails against your identity provider's roster. 3. Remove members who should no longer be in the group with `DELETE /v1/organizations/rbac_groups/{group_id}/members/{user_id}`. For `scim` groups, make the change in your identity provider instead. ## Frequently asked questions ### Is this a different API from the Admin API? No. The member and invite endpoints are the same `/v1/organizations/` endpoints that Claude Console organizations use; this page documents their Claude Enterprise behavior. The group and custom-role endpoints are part of the same API and exist only for Claude Enterprise organizations. The [availability table](https://platform.claude.com/docs/en/manage-claude/user-management#which-endpoints-can-your-organization-use) shows which endpoints each organization type can call. ### Can I assign the owner or membership admin role through the API? No. The API assigns only `user` and `managed`, on invite creation and role updates. Administrative roles are assigned in claude.ai organization settings, and members holding them cannot be modified or removed through the API. ### Can I create or modify groups through the API? Yes, with the `write:rbac_groups` scope: create, rename, and delete groups, and add or remove their members. Two things the API cannot change: groups provisioned by your identity provider (`source_type: "scim"`), whose name and membership are owned by the identity provider, and custom roles, which are managed in claude.ai organization settings (the API [reads them](https://platform.claude.com/docs/en/manage-claude/user-management#custom-roles)). ### Does an unaccepted invite consume a seat? On plans with a finite seat pool, yes: a `pending` invite holds a seat. Withdrawing the invite or letting it expire frees the seat. On plans without a seat pool, invites consume nothing. ### My organization uses single sign-on. Which operations work? If your identity provider provisions users automatically (JIT or SCIM), invite creation returns 400. If it manages roles (advanced SSO or advanced SCIM provisioning), role updates return 400. If it manages membership (SCIM provisioning), member removals return 400. Reads work regardless. ### What happens to an Admin API key when the person who created it leaves? The key keeps working. Admin API keys are scoped to the organization, not to individual users, and a key created in claude.ai does not expire. Removing the creator from the organization or deprovisioning them through your identity provider ends their own access, but not the keys they created. Downgrading their role does not change the keys either: each key stays active with its original scopes. When you offboard someone who created Admin API keys, delete those keys in the **Keys** section of [claude.ai > Organization settings > API](https://claude.ai/admin-settings/api-access) and create replacements. ## See also Where your primary owner creates a scoped key and which scopes to select. Audit activity and retrieve or delete user content across your organization. Per-user and time-bucketed usage and cost reporting for Claude Enterprise. Set per-member spend limits and review increase requests. --- title: Workspaces url: https://platform.claude.com/docs/en/manage-claude/workspaces description: Organize API keys, manage team access, and control costs with workspaces. --- Workspaces provide a way to organize your API usage within an organization. Use workspaces to separate different projects, environments, or teams while maintaining centralized billing and administration. ## How workspaces work Every organization has a **Default Workspace** that cannot be renamed, archived, or deleted. When you create additional workspaces, you can assign API keys, members, and resource limits to each one. Key characteristics: * **Workspace identifiers** use the `wrkspc_` prefix (for example, `wrkspc_01JwQvzr7rXLA5AGx3HKfFUJ`) * **Maximum 100 workspaces** per organization (archived workspaces don't count) * **Default Workspace** has a `wrkspc_` ID like any other workspace (returned in the [`anthropic-workspace-id` response header](https://platform.claude.com/docs/en/manage-claude/workspaces#identify-the-workspace-behind-an-api-response) and accepted by [Get Workspace](https://platform.claude.com/docs/en/api/admin/workspaces/retrieve)), but it doesn't appear in [List Workspaces](https://platform.claude.com/docs/en/api/admin/workspaces/list) results, and API keys, usage reports, and cost reports show `null` for its `workspace_id` * **API keys** are scoped to a single workspace and can only access resources within that workspace ### Claude Code workspace When a member of your organization first signs in to [Claude Code](https://code.claude.com/docs/en/overview) with their Claude Console account, Anthropic automatically creates a **Claude Code** workspace in the organization and adds that member to it. Every subsequent member who signs in to Claude Code is added the same way. The Claude Code workspace keeps Claude Code traffic separate from your other API workloads: * Claude Code mints a per-user API key in this workspace at sign-in. You cannot create keys in it manually from the Console. * A Claude Code key stops working if its owner is removed from the workspace or organization, unlike standard workspace keys. * Claude Code usage is rate-limited separately, and admins can cap its share of the organization's limits under [Settings > Workspaces](https://platform.claude.com/settings/workspaces). * It is the only workspace that supports per-user monthly spend limits. Archiving the Claude Code workspace disables Claude Code sign-in through Console billing for the whole organization. ## Workspace roles and permissions Members can have different roles in each workspace, allowing fine-grained access control. | Role | Permissions | | --------------------------- | ----------------------------------------------------------------------------------------------- | | Workspace User | Use the Anthropic Workbench only | | Workspace Limited Developer | Create and manage API keys, use the API. Cannot access session tracing views or download files. | | Workspace Developer | Create and manage API keys, use the API | | Workspace Admin | Full control over workspace settings and members | | Workspace Billing | View workspace billing information (inherited from organization billing role) | ### Role inheritance * **Organization admins** automatically receive Workspace Admin access to all workspaces * **Organization billing members** automatically receive Workspace Billing access to all workspaces * **Organization users and developers** must be explicitly added to each workspace The Workspace Billing role cannot be manually assigned. It's inherited from having the organization billing role. ## Managing workspaces Only organization admins can create workspaces. Organization users and developers must be added to workspaces by an admin. ### Using the Console Create and manage workspaces in the [Claude Console](https://platform.claude.com/settings/workspaces). #### Create a workspace In the Claude Console, go to **Settings > Workspaces**. Click **Create workspace**. Enter a workspace name and select a color for visual identification. Click **Create** to finalize. To switch between workspaces in the Console, use the **Workspaces** selector in the top-left corner. #### Edit workspace details To modify a workspace's name or color: 1. Select the workspace from the list. 2. Click the ellipsis menu (**...**) and choose **Edit details**. 3. Update the name or color and save your changes. The Default Workspace cannot be renamed or deleted. #### Add members to a workspace 1. Navigate to the workspace's **Members** tab. 2. Click **Add to Workspace**. 3. Select an organization member and assign them a [workspace role](https://platform.claude.com/docs/en/manage-claude/workspaces#workspace-roles-and-permissions). 4. Confirm the addition. To remove a member, click the trash icon next to their name. Organization admins and billing members cannot be removed from workspaces while they hold those organization roles. #### Set workspace limits Each workspace's settings split these across two tabs: * **Rate limits:** On the **Rate limits** tab, set limits per model tier for requests per minute, input tokens, or output tokens * **Spend limits:** On the **Spend limits** tab, cap monthly spending and configure alerts when spending reaches certain thresholds #### Archive a workspace To archive a workspace, click the ellipsis menu (**...**) and select **Archive**. Archiving: * Preserves historical data for reporting * Deactivates the workspace and all associated API keys * Cannot be undone Archiving a workspace immediately revokes all API keys in that workspace. This action cannot be undone. If you archive the [Claude Code workspace](https://platform.claude.com/docs/en/manage-claude/workspaces#claude-code-workspace), members of your organization can no longer sign in to Claude Code through Console billing. ### Using the Admin API Programmatically manage workspaces using the [Admin API](https://platform.claude.com/docs/en/manage-claude/admin-api). Admin API endpoints require an Admin API key (starting with `sk-ant-admin...`) that differs from standard API keys. See [Create an Admin API key](https://platform.claude.com/docs/en/manage-claude/admin-api-keys) for how to provision one. ```bash cURL # Create a workspace curl -X POST "https://api.anthropic.com/v1/organizations/workspaces" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" \ -d '{"name": "Production"}' # List workspaces curl "https://api.anthropic.com/v1/organizations/workspaces?limit=10&include_archived=false" \ -H "anthropic-version: 2023-06-01" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" # Archive a workspace curl -X POST "https://api.anthropic.com/v1/organizations/workspaces/{workspace_id}/archive" \ -H "anthropic-version: 2023-06-01" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" ``` For complete parameter details and response schemas, see the [Workspaces API reference](https://platform.claude.com/docs/en/api/admin/workspaces/retrieve). ### Managing workspace members Add, update, or remove members from a workspace: ```bash cURL # Add a member to a workspace curl -X POST "https://api.anthropic.com/v1/organizations/workspaces/{workspace_id}/members" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" \ -d '{ "user_id": "user_xxx", "workspace_role": "workspace_developer" }' # Update a member's role curl -X POST "https://api.anthropic.com/v1/organizations/workspaces/{workspace_id}/members/{user_id}" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" \ -d '{"workspace_role": "workspace_admin"}' # Remove a member from a workspace curl -X DELETE "https://api.anthropic.com/v1/organizations/workspaces/{workspace_id}/members/{user_id}" \ -H "anthropic-version: 2023-06-01" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" ``` For complete parameter details, see the [Workspace Members API reference](https://platform.claude.com/docs/en/api/admin/workspaces/members/retrieve). ## API keys and resource scoping API keys are scoped to a specific workspace. When you create an API key in a workspace, it can only access resources within that workspace. Resources scoped to workspaces include: * **Files** created through the [Files API](https://platform.claude.com/docs/en/build-with-claude/files) * **Message Batches** created through the [Batch API](https://platform.claude.com/docs/en/build-with-claude/batch-processing) * **Skills** created through the [Skills API](https://platform.claude.com/docs/en/build-with-claude/skills-guide) Some resources cannot be managed with a workspace API key: * **[MCP tunnels](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/overview)** are managed with a `workspace:manage_tunnels` OAuth token obtained through [Workload Identity Federation](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation), not a workspace API key. Tunnels are created in a workspace, and the Console **MCP tunnels** list and the Managed Agent server picker show tunnels in the current workspace only; the cap of 10 active tunnels applies organization-wide. Tunnel management requires a role with tunnel management permissions; organization developers can view but not change them. * **Workspaces** themselves and **organization members** are managed at the organization level through the [Admin API](https://platform.claude.com/docs/en/manage-claude/admin-api), which requires an Admin API key. To look up your organization's workspace IDs, call the [List Workspaces](https://platform.claude.com/docs/en/api/admin/workspaces/list) endpoint or find them in the [Claude Console](https://platform.claude.com/settings/workspaces). [Prompt caches](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) are also isolated per workspace on the Claude API, [Claude Platform on AWS](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws), and [Microsoft Foundry](https://platform.claude.com/docs/en/build-with-claude/claude-in-microsoft-foundry). On Amazon Bedrock and Google Cloud, prompt caches are isolated per organization. ## Identify the workspace behind an API response Claude API responses include an `anthropic-workspace-id` header alongside the `request-id` and `anthropic-organization-id` [response headers](https://platform.claude.com/docs/en/api/overview#response-headers). Its value is the `wrkspc_`-prefixed ID of the workspace that the request's API key or access token resolved to, including when that workspace is the Default Workspace. For example, a successful response includes headers like these: ```http HTTP/1.1 200 OK request-id: req_018EeWyXxfu5pfWkrYcMdjWG anthropic-organization-id: 0d0e7a3b-52f1-4c7e-9a51-3f6f2f7c1b9e anthropic-workspace-id: wrkspc_01JwQvzr7rXLA5AGx3HKfFUJ ``` The header is absent when the credential doesn't resolve to a workspace (for example, on Admin API requests) or when the request fails before authentication completes, such as a 401 error. The following examples send a Messages API request and print the workspace ID from the response headers: ```bash cURL # -D - prints the response headers; -o /dev/null discards the body curl -sS -D - -o /dev/null https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "messages": [{"role": "user", "content": "Hello, Claude"}] }' | grep -i '^anthropic-workspace-id' ``` ```bash CLI # --debug prints the HTTP response, including the Anthropic-Workspace-Id # header, to stderr; > /dev/null hides the JSON body on stdout ant --debug messages create \ --model claude-opus-5 \ --max-tokens 1024 \ --message '{role: user, content: "Hello, Claude"}' > /dev/null ``` ```python Python client = anthropic.Anthropic() response = client.messages.with_raw_response.create( model="claude-opus-5", max_tokens=1024, messages=[{"role": "user", "content": "Hello, Claude"}], ) workspace_id = response.headers.get("anthropic-workspace-id") print(f"Workspace ID: {workspace_id}") ``` ```typescript TypeScript const client = new Anthropic(); const { response } = await client.messages .create({ model: "claude-opus-5", max_tokens: 1024, messages: [{ role: "user", content: "Hello, Claude" }] }) .withResponse(); console.log("Workspace ID:", response.headers.get("anthropic-workspace-id")); ``` ```csharp C# AnthropicClient client = new(); using var response = await client.WithRawResponse.Messages.Create(new() { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [new() { Role = Role.User, Content = "Hello, Claude" }] }); var workspaceId = response.GetHeaderValues("anthropic-workspace-id").First(); Console.WriteLine($"Workspace ID: {workspaceId}"); ``` ```go Go client := anthropic.NewClient() var response *http.Response _, err := client.Messages.New( context.Background(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Hello, Claude")), }, }, option.WithResponseInto(&response), ) if err != nil { log.Fatal(err) } fmt.Println("Workspace ID:", response.Header.Get("anthropic-workspace-id")) ``` ```java Java import com.anthropic.client.AnthropicClient; import com.anthropic.client.okhttp.AnthropicOkHttpClient; import com.anthropic.core.http.HttpResponseFor; import com.anthropic.models.messages.Message; import com.anthropic.models.messages.MessageCreateParams; import com.anthropic.models.messages.Model; void main() { AnthropicClient client = AnthropicOkHttpClient.fromEnv(); HttpResponseFor response = client.messages().withRawResponse().create( MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .addUserMessage("Hello, Claude") .build() ); String workspaceId = response.headers().values("anthropic-workspace-id").getFirst(); IO.println("Workspace ID: " + workspaceId); } ``` ```php PHP $client = new Client(); $response = $client->messages->raw->create([ 'model' => Model::CLAUDE_OPUS_5, 'maxTokens' => 1024, 'messages' => [['role' => 'user', 'content' => 'Hello, Claude']], ]); echo 'Workspace ID: ' . $response->getHeaderLine('anthropic-workspace-id') . "\n"; ``` ```ruby Ruby client = Anthropic::Client.new # Read response headers in per-request middleware, which receives the # raw HTTP response before the SDK parses it workspace_id = nil read_workspace_id = lambda do |request, call_next| response = call_next.call(request) # Keys in response.headers are lowercase workspace_id = response.headers["anthropic-workspace-id"] response end client.messages.create( model: Anthropic::Model::CLAUDE_OPUS_5, max_tokens: 1024, messages: [{ role: "user", content: "Hello, Claude" }], request_options: { middleware: [read_workspace_id] } ) puts "Workspace ID: #{workspace_id}" ``` ```text Output wrap Workspace ID: wrkspc_01JwQvzr7rXLA5AGx3HKfFUJ ``` The same accessors read the header from other Claude API endpoints too, including the [Claude Managed Agents](https://platform.claude.com/docs/en/managed-agents/overview) APIs. For example, read `anthropic-workspace-id` from the response that [creates a session](https://platform.claude.com/docs/en/managed-agents/sessions) to record which workspace the session belongs to. With the workspace ID from a response, you can: * Confirm which workspace's usage, cost, and [rate limits](https://platform.claude.com/docs/en/api/rate-limits) the request counted toward * Match it against the `workspace_id` field in [Usage and Cost API](https://platform.claude.com/docs/en/manage-claude/usage-cost-api) reports and on [Admin API](https://platform.claude.com/docs/en/manage-claude/admin-api) objects such as API keys (both report `null` for the Default Workspace) * Check whether it's your Default Workspace's ID by passing it to [Get Workspace](https://platform.claude.com/docs/en/api/admin/workspaces/retrieve) with an [Admin API key](https://platform.claude.com/docs/en/manage-claude/admin-api-keys): the Default Workspace comes back with `"name": "Default"`, even though [List Workspaces](https://platform.claude.com/docs/en/api/admin/workspaces/list) omits it * Open that workspace in the [Console](https://platform.claude.com/settings/workspaces) to find the request's resources, such as sessions, files, message batches, and skills ## Workspace limits You can set custom spend and rate limits for each workspace to protect against overuse and ensure fair resource distribution. ### Setting workspace limits You can set workspace limits lower than (but not higher than) your organization's limits: * **Spend limits:** Cap monthly spending for a workspace. Set these on the workspace's **Spend limits** settings tab in the [Claude Console](https://platform.claude.com/settings/workspaces). * **Rate limits:** Limit requests per minute, input tokens per minute, or output tokens per minute. Set these on the workspace's **Rate limits** settings tab in the [Claude Console](https://platform.claude.com/settings/workspaces). - You cannot set limits on the Default Workspace - If not set, workspace limits match the organization's limits - Organization-wide limits always apply, even if workspace limits add up to more For detailed information on rate limits and how they work, see [Rate limits](https://platform.claude.com/docs/en/api/rate-limits). You can also read your current organization and workspace rate limits programmatically with the [Rate Limits API](https://platform.claude.com/docs/en/manage-claude/rate-limits-api). ## Usage and cost tracking Track usage and costs by workspace using the [Usage and Cost API](https://platform.claude.com/docs/en/manage-claude/usage-cost-api): ```bash cURL curl "https://api.anthropic.com/v1/organizations/usage_report/messages?\ starting_at=2025-01-01T00:00:00Z&\ ending_at=2025-01-08T00:00:00Z&\ workspace_ids[]=wrkspc_01JwQvzr7rXLA5AGx3HKfFUJ&\ group_by[]=workspace_id&\ bucket_width=1d" \ -H "anthropic-version: 2023-06-01" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" ``` Usage and costs attributed to the Default Workspace have a `null` value for `workspace_id`. ## Common use cases ### Environment separation Create separate workspaces for development, staging, and production: | Workspace | Purpose | | ----------- | -------------------------------------------------- | | Development | Testing and experimentation with lower rate limits | | Staging | Pre-production testing with production-like limits | | Production | Live traffic with full rate limits and monitoring | ### Team or department isolation Assign workspaces to different teams for cost allocation and access control: * **Engineering team** with developer access * **Data science team** with their own API keys * **Support team** with limited access for customer tools ### Project-based organization Create workspaces for specific projects or products to track usage and costs separately. ## Best practices Consider how you'll organize workspaces before creating them. Think about billing, access control, and usage tracking needs. Name workspaces clearly to indicate their purpose (for example, "Production - Customer Chatbot" or "Dev - Internal Tools"). Configure spend and rate limits to prevent unexpected costs and ensure fair resource distribution. Review workspace membership periodically to ensure only appropriate users have access. Use the [Usage and Cost API](https://platform.claude.com/docs/en/manage-claude/usage-cost-api) to track workspace-level consumption. ## FAQ Every organization has a "Default Workspace" that cannot be renamed, archived, or deleted. Like every workspace, it has a `wrkspc_` ID: the API returns it in the [`anthropic-workspace-id` response header](https://platform.claude.com/docs/en/manage-claude/workspaces#identify-the-workspace-behind-an-api-response), and you can pass it to [Get Workspace](https://platform.claude.com/docs/en/api/admin/workspaces/retrieve) and [Update Workspace](https://platform.claude.com/docs/en/api/admin/workspaces/update). It has no member list of its own, because access to it follows each member's organization role. It doesn't appear in [List Workspaces](https://platform.claude.com/docs/en/api/admin/workspaces/list) results, and API keys, usage reports, and cost reports that belong to it show `null` for `workspace_id`. Anthropic creates the Claude Code workspace automatically the first time a member of your organization signs in to Claude Code with their Console account. It isolates Claude Code's API keys, usage, and rate limits from your other workloads. See [Claude Code workspace](https://platform.claude.com/docs/en/manage-claude/workspaces#claude-code-workspace) for details. Yes, you can have a maximum of 100 workspaces per organization. Archived workspaces do not count toward this limit. Organization admins automatically get the Workspace Admin role in all workspaces. Organization billing members automatically get the Workspace Billing role. Organization users and developers must be manually added to each workspace. Organization users and developers can be assigned Workspace Admin, Workspace Developer, Workspace Limited Developer, or Workspace User roles. The Workspace Billing role cannot be manually assigned; it's inherited from having the organization `billing` role. Organization admins and billing members cannot have their workspace roles changed or be removed from workspaces while they hold those organization roles (with one exception: billing members can be upgraded to a Workspace Admin role). For everyone else covered by this constraint, change their organization role first to change their workspace access. If an organization admin or billing member is demoted to user or developer, they lose access to all workspaces except ones where they were manually assigned roles. When users are promoted to admin or billing roles, they gain automatic access to all workspaces. API keys persist in their current state as they are scoped to the organization and workspace, not to individual users. The exception is the [Claude Code workspace](https://platform.claude.com/docs/en/manage-claude/workspaces#claude-code-workspace), where each key is bound to the member who created it and stops working when that member is removed. ## See also * [Admin API](https://platform.claude.com/docs/en/manage-claude/admin-api) * [Admin API reference](https://platform.claude.com/docs/en/api/admin) * [Rate limits](https://platform.claude.com/docs/en/api/rate-limits) * [Usage and Cost API](https://platform.claude.com/docs/en/manage-claude/usage-cost-api) ### Authentication --- title: App Attest for iOS and macOS apps url: https://platform.claude.com/docs/en/manage-claude/app-attest description: Let genuine installations of your iOS or macOS app call the Claude API without shipping an API key or running a proxy, using Apple's App Attest service. --- App Attest authenticates iOS and macOS apps that call the Claude API directly from the device, with usage billed to your workspace. This page explains how App Attest works, how to register your app in the Claude Console, and how to revoke an app integration. Apps use App Attest through the [Claude for Foundation Models](https://github.com/anthropics/ClaudeForFoundationModels) Swift package, which is in beta: it requires the OS 27 betas, and APIs might change before general availability. For the Swift configuration, see [Apple Foundation Models](https://platform.claude.com/docs/en/cli-sdks-libraries/libraries/apple-foundation-models#app-attest-production). ## How App Attest works Each installation of your app uses Apple's [App Attest](https://developer.apple.com/documentation/devicecheck/establishing-your-app-s-integrity) service to prove that it is a genuine, unmodified build of the app you registered. Anthropic then issues the device a short-lived access token that bills usage to your workspace. The app ships no API key, and there is no proxy for you to operate. App Attest authentication is available only when your app calls the Claude API directly. It is not available through Amazon Bedrock, Google Cloud, or Microsoft Foundry. The first time your app uses Claude on a device, the app requests a challenge from Anthropic, attests the device with Apple's `DCAppAttestService`, and exchanges the verified attestation for an access token. The Claude for Foundation Models package runs this flow automatically and requests new tokens as they expire; there is no attestation code for you to write. Tokens are scoped to your workspace, expire after one hour, and authorize only [Messages API](https://platform.claude.com/docs/en/api/messages/create) calls. They carry no end-user identity: App Attest identifies your app, not the person using it, so handle any per-user logic in your app. ## Set up App Attest App Attest requires a physical device. The Simulator, and hardware without a Secure Enclave, cannot perform App Attest. While developing in the Simulator, authenticate with an [API key](https://platform.claude.com/docs/en/manage-claude/authentication#api-keys) instead. To set up App Attest, you need your Apple Developer Team ID and the admin, owner, or primary owner role in your organization. Configure your Xcode project and register your app in the [Claude Console](https://platform.claude.com/): 1. In Xcode, add the **App Attest** capability to your app target under **Signing & Capabilities**. 2. In your workspace's settings in the Claude Console, open **App integrations**. 3. Click **Create app integration** and enter a name, your Apple Developer Team ID, and one or more bundle IDs (up to 32). 4. Copy the client ID (`clid_...`) from the integration's **Overview** tab and pass it to your app's Claude configuration. ## Revoke an app integration To stop a compromised or retired app, revoke its integration: in your workspace's settings in the Claude Console, open **App integrations**, select the integration, and click **Revoke**, then confirm. Revoking an integration revokes its outstanding tokens, and its registered devices can no longer request new ones. Revocation is permanent, so create a new app integration to restore access. ## Next steps Configure App Attest in the Claude for Foundation Models Swift package Compare API keys, Workload Identity Federation, and App Attest --- title: Create an Admin API key url: https://platform.claude.com/docs/en/manage-claude/admin-api-keys description: Create an Admin API key for your Claude Console or Claude Enterprise organization. --- An Admin API key authenticates every API in the **Admin** section of this guide: the [Admin API](https://platform.claude.com/docs/en/manage-claude/admin-api), [Analytics APIs](https://platform.claude.com/docs/en/manage-claude/analytics-api), [Compliance API](https://platform.claude.com/docs/en/manage-claude/compliance-api), [Spend Limits API](https://platform.claude.com/docs/en/manage-claude/spend-limits-api), [Usage and Cost API](https://platform.claude.com/docs/en/manage-claude/usage-cost-api), and [Rate Limits API](https://platform.claude.com/docs/en/manage-claude/rate-limits-api). You do not need a separate key for each API. The one exception is the Admin API's service-account, federation-issuer, and federation-rule endpoints, which accept only an OAuth bearer token with the `org:admin` scope. See [Obtain an OAuth bearer token](https://platform.claude.com/docs/en/manage-claude/admin-api#oauth-bearer-token). Where you create the key depends on which Claude product your organization uses. ## Which key do you need? | Your organization | Create the key in | Key prefix | Who can create it | Works with | | ----------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Claude Console** (Claude Platform, `platform.claude.com`) | [Claude Console > Settings > Admin keys](https://platform.claude.com/settings/admin-keys) | `sk-ant-admin01-...` | Organization members with the **admin** role | [Admin API](https://platform.claude.com/docs/en/manage-claude/admin-api), [Usage and Cost API](https://platform.claude.com/docs/en/manage-claude/usage-cost-api), [Rate Limits API](https://platform.claude.com/docs/en/manage-claude/rate-limits-api), [Claude Code Analytics API](https://platform.claude.com/docs/en/manage-claude/claude-code-analytics-api), and the Compliance API [Activity Feed](https://platform.claude.com/docs/en/manage-claude/compliance-activity-feed) | | **Claude Enterprise** (`claude.ai`) | [claude.ai > Organization settings > API](https://claude.ai/admin-settings/api-access) | `sk-ant-api01-...` | The parent organization's **primary owner** (all linked organizations). An **organization owner** can create one carrying Compliance API scopes only, restricted to their own organization | [User management](https://platform.claude.com/docs/en/manage-claude/user-management) (the Admin API's member, invite, and group endpoints, in beta), [Compliance API](https://platform.claude.com/docs/en/manage-claude/compliance-api), [Claude Enterprise Analytics API](https://platform.claude.com/docs/en/manage-claude/analytics-api), and [Spend Limits API](https://platform.claude.com/docs/en/manage-claude/spend-limits-api), according to the [scopes](https://platform.claude.com/docs/en/manage-claude/admin-api-keys#choose-scopes-for-a-claude-enterprise-key) you select | A key created in one organization cannot be used to manage a different organization. If your company uses both Claude Console and Claude Enterprise, create one key in each. ## Create a key for a Claude Console organization Only organization members with the **admin** role can create Admin API keys. See [Organization roles and permissions](https://platform.claude.com/docs/en/manage-claude/admin-api#organization-roles-and-permissions). Go to [Claude Console > Settings > Admin keys](https://platform.claude.com/settings/admin-keys). Click **Create key**, give it a name, choose a [key expiration](https://platform.claude.com/docs/en/manage-claude/authentication#key-expiration), and click **Create**. Claude Console keys do not have selectable scopes; every key carries full access to all endpoints that accept Admin API keys (the service-account and federation endpoints noted at the top of this page do not accept Admin API keys). Copy the displayed secret (starting with `sk-ant-admin01-`) and store it in your secrets manager. The full secret is shown only once. ## Create a key for a Claude Enterprise organization The **primary owner** of the Claude Enterprise parent organization can create a key that can access every linked organization, or one restricted to a single organization. An **organization owner** can create a key with Compliance API scopes only, restricted to their own organization. Go to [claude.ai > Organization settings > API](https://claude.ai/admin-settings/api-access) and find the **Keys** section. Name the key and select the scopes you need from the [scopes table](https://platform.claude.com/docs/en/manage-claude/admin-api-keys#choose-scopes-for-a-claude-enterprise-key). The primary owner can combine scopes from different APIs (for example, `read:analytics` and `read:spend_limits`) on a single key. Copy the displayed secret (starting with `sk-ant-api01-`) and store it in your secrets manager. The full secret is shown only once. ## Choose scopes for a Claude Enterprise key When you create a Claude Enterprise key, select every scope that the APIs you plan to call require. Scopes are fixed at creation; to add a scope later, create a new key. | To call... | Select these scopes | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- | | Admin API [user management](https://platform.claude.com/docs/en/manage-claude/user-management): list and look up members and invites; read custom roles and their permissions | `read:members` | | Admin API [user management](https://platform.claude.com/docs/en/manage-claude/user-management): change member roles, remove members, create and withdraw invites | `write:members` | | Admin API [user management](https://platform.claude.com/docs/en/manage-claude/user-management): read groups and their members | `read:rbac_groups` | | Admin API [user management](https://platform.claude.com/docs/en/manage-claude/user-management): create, rename, and delete groups; add and remove group members; assign groups on invite creation | `write:rbac_groups` | | [Spend Limits API](https://platform.claude.com/docs/en/manage-claude/spend-limits-api): read members' effective spend limits and increase requests | `read:spend_limits` | | [Spend Limits API](https://platform.claude.com/docs/en/manage-claude/spend-limits-api): set or clear per-user spend limits; approve or deny increase requests | `write:spend_limits` | | [Claude Enterprise Analytics API](https://platform.claude.com/docs/en/manage-claude/analytics-api): engagement, adoption, cost, and usage reports | `read:analytics` | | [Compliance API Activity Feed](https://platform.claude.com/docs/en/manage-claude/compliance-activity-feed): organization-wide activity events | `read:compliance_activities` | | [Compliance API chat, file, and project endpoints](https://platform.claude.com/docs/en/manage-claude/compliance-content-data) and [Compliance API session endpoints](https://platform.claude.com/docs/en/manage-claude/compliance-sessions): read chats, files, projects, Cowork and Claude Code session transcripts, and [organization users](https://platform.claude.com/docs/en/manage-claude/compliance-org-data#list-organization-users) | `read:compliance_user_data` | | [Compliance API chat, file, and project endpoints](https://platform.claude.com/docs/en/manage-claude/compliance-content-data): delete chats, files, and projects | `delete:compliance_user_data` | | [Compliance API organization endpoints](https://platform.claude.com/docs/en/manage-claude/compliance-org-data): read organization metadata and effective settings | `read:compliance_org_data` | | Admin API [user management](https://platform.claude.com/docs/en/manage-claude/user-management) read endpoints and every Compliance API read endpoint, with a single read-only scope (for security-audit integrations; does not include the Spend Limits or Analytics APIs) | `read:org_audit` | The Compliance and Analytics APIs must be enabled for your organization before keys with those scopes can be used. See [Set up the Compliance API](https://platform.claude.com/docs/en/manage-claude/compliance-api-access#set-up-the-compliance-api) and [Get access to the Claude Enterprise Analytics API](https://platform.claude.com/docs/en/manage-claude/analytics-api#get-access-to-the-claude-enterprise-analytics-api). ## Use the key Pass the key in the `x-api-key` header on every request. See each API's documentation for complete request examples. A call that exceeds the key's scopes returns `403 Forbidden` with a message listing the scopes the key has and the scopes the endpoint needs. ## Next steps Manage organization members, workspaces, and API keys. Set per-member spend limits and review increase requests for your Claude Enterprise organization. Report on Claude Code productivity or Claude Enterprise engagement and adoption. Audit activity and retrieve or delete user content across your organization. --- title: Manage WIF with the Admin API url: https://platform.claude.com/docs/en/manage-claude/wif-admin-api description: Create and manage Workload Identity Federation service accounts, issuers, and rules programmatically for infrastructure-as-code and CI workflows. --- The Admin API lets you create and manage [Workload Identity Federation](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation) resources programmatically: service accounts, federation issuers, and federation rules. Use it to keep your federation configuration in infrastructure as code, provision it from CI, and reproduce it across organizations instead of clicking through the Claude Console. These endpoints share the `/v1/organizations` path prefix with the rest of the [Admin API](https://platform.claude.com/docs/en/manage-claude/admin-api). ## Prerequisites Every request on this page authenticates with an OAuth bearer token that carries the `org:admin` scope. The scope is granted only to organization members with the admin, owner, or primary owner role, and it grants access to the whole organization: any workspace binding is ignored. There are two ways to obtain a token, and they carry different permissions: a token from your own login acts as a user, whereas a federated token acts as a service account and cannot perform every operation on this page. ### Interactive (your terminal) Log in with the [`ant` CLI](https://platform.claude.com/docs/en/cli-sdks-libraries/cli/quickstart) under a dedicated profile, requesting the `org:admin` scope (see [Admin access](https://platform.claude.com/docs/en/cli-sdks-libraries/cli/authentication#admin-access)), then export the bearer token: ```bash CLI ant auth login --profile admin --scope "org:admin" export ANTHROPIC_OAUTH_TOKEN=$(ant auth print-credentials --profile admin --access-token) ``` Interactive tokens are short-lived; if requests start returning 401, re-run the export command (it refreshes the token automatically). ### Workload (CI and automation) Create a federation rule with `oauth_scope: org:admin` that targets a service account whose `organization_role` is `admin`. The rule itself must be created in the Claude Console: granting a workload organization-admin access is a deliberate human action, not something automation can bootstrap for itself. The next section walks through this once-per-organization setup. ## Bootstrap a workload to manage WIF One Console-created rule is enough to put the rest of your federation configuration under infrastructure as code: grant a single trusted workload the `org:admin` scope, and let that workload manage federation issuers and every workspace-scoped federation rule through this API. In the Claude Console, go to **Settings → Workload identity** and select **Connect workload** to create one federation rule for your automation workload, for example a GitHub Actions workflow in your infrastructure repository. Under **Advanced rule options**, set the rule's OAuth scope to `org:admin`: the wizard then creates the new service account with the Admin organization role (or asks you to pick an existing admin service account as the target). Match the rule to one exact workload identity, not a broad pattern. `subject_prefix` is an exact match unless it ends in `*`. For GitHub Actions, pin the subject to a protected branch, such as `repo:my-org/my-repo:ref:refs/heads/main`. A trailing wildcard such as `repo:my-org/my-repo:*` also matches `pull_request` runs, including runs triggered from forks, so anyone who could open a pull request against the repository could mint an `org:admin` token. See [Restrict which workflows can authenticate](https://platform.claude.com/docs/en/manage-claude/wif-providers/github-actions#restrict-which-workflows-can-authenticate). At runtime, the workload exchanges the JWT from its identity provider for a short-lived `org:admin` bearer token using the same [token exchange](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation#authenticate-from-your-workload) as any other federated workload. With the minted token in `ANTHROPIC_OAUTH_TOKEN`, the workload creates and manages your federation configuration using the endpoints on this page. For the operations a workload-minted token can and cannot perform, see [Permissions and constraints](https://platform.claude.com/docs/en/manage-claude/wif-admin-api#permissions-and-constraints). If you already created issuers, service accounts, or rules with the **Connect workload** wizard, list them with the following endpoints and import them into your infrastructure-as-code state instead of recreating them. ## Authentication All endpoints live under `https://api.anthropic.com/v1/organizations/`. Every request to the federation and service-account endpoints needs the API version header and the bearer token: ```bash cURL curl --fail-with-body -sS "https://api.anthropic.com/v1/organizations/service_accounts" \ -H "anthropic-version: 2023-06-01" \ -H "authorization: Bearer $ANTHROPIC_OAUTH_TOKEN" ``` Admin API keys are not accepted on these endpoints; the Admin API page's `x-api-key` examples do not apply here. ## Service accounts A [service account](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation#service-accounts) (`svac_...`) is the non-human identity that a federated token acts as. Set `organization_role` to `developer`. ```bash cURL # Create a service account curl --fail-with-body -sS "https://api.anthropic.com/v1/organizations/service_accounts" \ -H "anthropic-version: 2023-06-01" \ -H "authorization: Bearer $ANTHROPIC_OAUTH_TOKEN" \ -H "content-type: application/json" \ -d '{ "name": "inference-worker", "organization_role": "developer" }' # List service accounts curl --fail-with-body -sS "https://api.anthropic.com/v1/organizations/service_accounts?limit=20" \ -H "anthropic-version: 2023-06-01" \ -H "authorization: Bearer $ANTHROPIC_OAUTH_TOKEN" # Archive a service account curl --fail-with-body -sS -X POST "https://api.anthropic.com/v1/organizations/service_accounts/svac_.../archive" \ -H "anthropic-version: 2023-06-01" \ -H "authorization: Bearer $ANTHROPIC_OAUTH_TOKEN" ``` The create endpoint returns the new service account: ```json { "id": "svac_...", "name": "inference-worker", "organization_role": "developer", "created_at": "...", "type": "service_account", "...": "..." } ``` To read or update a single service account, use `GET` and `POST` on `/v1/organizations/service_accounts/{service_account_id}`. A service account must be a member of a workspace before federated tokens can act in it. Every service account has an implicit membership in your organization's default workspace; add explicit memberships for other workspaces with `GET`, `POST`, and `DELETE` on `/v1/organizations/service_accounts/{service_account_id}/workspaces`, where `DELETE` targets `.../workspaces/{workspace_id}`. For complete parameter details and response schemas, see the [Service accounts API reference](https://platform.claude.com/docs/en/api/admin/service_accounts). ## Federation issuers A [federation issuer](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation#federation-issuers) (`fdis_...`) registers an OIDC identity provider with your organization. The `jwks` field is a discriminated union that controls how Anthropic fetches the provider's signing keys: | `jwks` value | When to use | | ---------------------------------------- | --------------------------------------------------------------------------------- | | `{"type": "discovery"}` | The provider serves `/.well-known/openid-configuration` at the issuer URL. | | `{"type": "explicit_url", "url": "..."}` | Point at a JWKS endpoint directly. | | `{"type": "inline", "keys": [...]}` | Upload the key set for providers that are not reachable from the public internet. | ```bash cURL # Register an issuer (GitHub Actions, with JWKS discovery) curl --fail-with-body -sS "https://api.anthropic.com/v1/organizations/federation_issuers" \ -H "anthropic-version: 2023-06-01" \ -H "authorization: Bearer $ANTHROPIC_OAUTH_TOKEN" \ -H "content-type: application/json" \ -d '{ "name": "github-actions", "issuer_url": "https://token.actions.githubusercontent.com", "jwks": {"type": "discovery"} }' # List issuers curl --fail-with-body -sS "https://api.anthropic.com/v1/organizations/federation_issuers?limit=20" \ -H "anthropic-version: 2023-06-01" \ -H "authorization: Bearer $ANTHROPIC_OAUTH_TOKEN" # Archive an issuer curl --fail-with-body -sS -X POST "https://api.anthropic.com/v1/organizations/federation_issuers/fdis_.../archive" \ -H "anthropic-version: 2023-06-01" \ -H "authorization: Bearer $ANTHROPIC_OAUTH_TOKEN" ``` To read or update a single issuer, use `GET` and `POST` on `/v1/organizations/federation_issuers/{issuer_id}`. An OAuth caller cannot update an issuer that backs a rule whose `oauth_scope` is anything other than `workspace:developer` or `workspace:inference`; see [Permissions and constraints](https://platform.claude.com/docs/en/manage-claude/wif-admin-api#permissions-and-constraints). For complete parameter details and response schemas, see the [Federation issuers API reference](https://platform.claude.com/docs/en/api/admin/federation_issuers). ## Federation rules A [federation rule](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation#federation-rules) (`fdrl_...`) binds an issuer to a service account: JWTs from the issuer that satisfy the rule's match conditions can mint tokens that act as the rule's target. The `workspace_id` in the create request enables the rule in that workspace at creation; add more workspaces later through the `/federation_rules/{rule_id}/workspaces` sub-resource. Either `workspace_id` or `applies_to_all_workspaces: true` is required on create. ```bash cURL # Create a rule (GitHub Actions deploys from the main branch) curl --fail-with-body -sS "https://api.anthropic.com/v1/organizations/federation_rules" \ -H "anthropic-version: 2023-06-01" \ -H "authorization: Bearer $ANTHROPIC_OAUTH_TOKEN" \ -H "content-type: application/json" \ -d '{ "name": "gha-deploy", "issuer_id": "fdis_...", "match": { "subject_prefix": "repo:my-org/my-repo:ref:refs/heads/main", "claims": {"repository_owner": "my-org"} }, "target": { "type": "service_account", "service_account_id": "svac_..." }, "workspace_id": "wrkspc_...", "oauth_scope": "workspace:developer", "token_lifetime_seconds": 600 }' # List rules, optionally filtered by issuer curl --fail-with-body -sS "https://api.anthropic.com/v1/organizations/federation_rules?issuer_id=fdis_..." \ -H "anthropic-version: 2023-06-01" \ -H "authorization: Bearer $ANTHROPIC_OAUTH_TOKEN" # Archive a rule curl --fail-with-body -sS -X POST "https://api.anthropic.com/v1/organizations/federation_rules/fdrl_.../archive" \ -H "anthropic-version: 2023-06-01" \ -H "authorization: Bearer $ANTHROPIC_OAUTH_TOKEN" ``` The list endpoint returns a page of rules and the cursor for the next page: ```json { "data": [{ "id": "fdrl_...", "name": "gha-deploy", "...": "..." }], "next_page": "..." } ``` To read or update a single rule, use `GET` and `POST` on `/v1/organizations/federation_rules/{rule_id}`. To manage the workspaces a rule can mint tokens in, use `GET` and `POST` on `/v1/organizations/federation_rules/{rule_id}/workspaces`, and `DELETE` on `/v1/organizations/federation_rules/{rule_id}/workspaces/{workspace_id}`. For complete parameter details and response schemas, see the [Federation rules API reference](https://platform.claude.com/docs/en/api/admin/federation_rules). ## Permissions and constraints * OAuth-authenticated callers can only create or modify rules whose `oauth_scope` is `workspace:developer` or `workspace:inference`. To create or modify a rule with any other scope (such as `org:admin` or `workspace:manage_tunnels`), use the Console. * An OAuth caller cannot update a federation issuer that backs a rule whose `oauth_scope` is anything other than `workspace:developer` or `workspace:inference` (such as `org:admin` or `workspace:manage_tunnels`). Consider registering a dedicated issuer for the bootstrap rule so the issuers behind workspace-scoped rules stay updatable through the API. * Admin API keys are not accepted on these endpoints, for reads or writes; use an `org:admin` OAuth token. A rule with `oauth_scope: org:admin` must target a service account whose `organization_role` is `admin`. Resource names must match `^[a-z0-9-]+$`, be 1 to 255 characters, and be unique within an organization for each resource type; for the full field-level constraints, see [Validation rules](https://platform.claude.com/docs/en/manage-claude/wif-reference#validation-rules). ## Pagination and archiving The service-account, federation-issuer, and federation-rule list endpoints accept `limit` (1 to 100, default 20) and a `page` cursor taken from the previous response. Pass the response's `next_page` value as the `page` query parameter on the next request. The rule-workspaces sub-resource list returns the full set without pagination. Archived resources are hidden from lists by default; pass `include_archived=true` to include them. Archiving is a soft delete and is idempotent: archiving an already-archived resource succeeds. Archiving an issuer or a service account returns `400` while a live federation rule still references it; archive the rule first. ## See also * [Workload Identity Federation](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation): concepts and the Console setup walkthrough * [WIF reference](https://platform.claude.com/docs/en/manage-claude/wif-reference): environment variables, validation rules, OAuth scopes, and error codes * [Admin API](https://platform.claude.com/docs/en/manage-claude/admin-api): the rest of the organization management surface * [Admin API reference](https://platform.claude.com/docs/en/api/admin): generated request and response schemas for every Admin API endpoint --- title: WIF reference url: https://platform.claude.com/docs/en/manage-claude/wif-reference description: Environment variables, validation rules, profile configuration, and error reference for Workload Identity Federation. --- This page collects the configuration surfaces, validation constraints, and error mappings for [Workload Identity Federation](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation). For setup walkthroughs, see the [provider guides](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation#identity-providers). ## Token exchange request `POST /v1/oauth/token` accepts a JSON body using the [RFC 7523](https://www.rfc-editor.org/rfc/rfc7523) `jwt-bearer` grant. The SDKs build this request for you from the [environment variables](https://platform.claude.com/docs/en/manage-claude/wif-reference#environment-variables); the cURL examples on each provider guide show the raw body. | Field | Required | Description | | -------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `grant_type` | Yes | Always `urn:ietf:params:oauth:grant-type:jwt-bearer`. | | `assertion` | Yes | The OIDC JWT issued by your identity provider. | | `federation_rule_id` | Yes | Tagged ID (`fdrl_...`) of the federation rule to evaluate. | | `organization_id` | Yes | UUID of your Anthropic organization. | | `service_account_id` | Yes | Tagged ID (`svac_...`) of the target service account. | | `workspace_id` | Conditional | Tagged ID (`wrkspc_...`) of the workspace to scope the minted token to, or the literal `default` for the organization's default workspace. Required when the rule is enabled for more than one workspace. When omitted, the server selects the rule's sole enabled workspace. | ## Token exchange response `POST /v1/oauth/token` returns a standard OAuth 2.0 token response ([RFC 6749 §5.1](https://www.rfc-editor.org/rfc/rfc6749#section-5.1)): | Field | Type | Description | | -------------- | ------- | --------------------------------------------------------------------------------------------------------- | | `access_token` | string | The short-lived Anthropic token, prefixed `sk-ant-oat01-...`. Pass it as `Authorization: Bearer `. | | `token_type` | string | Always `Bearer`. | | `expires_in` | integer | Seconds until the token expires. | | `scope` | string | The OAuth scope granted by the matched rule. | ## Environment variables The SDK reads these variables to perform a federated token exchange with no constructor arguments. | Variable | Required | Description | Example | | ------------------------------- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | | `ANTHROPIC_FEDERATION_RULE_ID` | Yes | Tagged ID of the federation rule to evaluate. | `fdrl_...` | | `ANTHROPIC_ORGANIZATION_ID` | Yes | UUID of your Anthropic organization. Find it in the Claude Console under **Settings > Organization**. | `00000000-0000-0000-0000-000000000000` | | `ANTHROPIC_IDENTITY_TOKEN_FILE` | One of `_TOKEN_FILE` or `_TOKEN` | Filesystem path to the JWT issued by your identity provider (IdP). The SDK re-reads this file on every exchange so that projected tokens that rotate on disk are always current. | `/var/run/secrets/anthropic.com/token` | | `ANTHROPIC_IDENTITY_TOKEN` | One of `_TOKEN_FILE` or `_TOKEN` | The literal JWT as a string. Use when your platform injects the token as an environment variable rather than a file. | `eyJhbGciOiJSUzI1NiIs...` | | `ANTHROPIC_SERVICE_ACCOUNT_ID` | Yes | Tagged ID of the target Anthropic service account that the issued access token acts as. | `svac_...` | | `ANTHROPIC_WORKSPACE_ID` | Conditional | Tagged ID of the workspace to scope the minted token to, or the literal `default`. Required when the federation rule is enabled for more than one workspace; optional when the rule is bound to a single workspace. The minted token is scoped to this workspace at exchange time, so switching workspaces requires a new exchange. | `wrkspc_...` | | `ANTHROPIC_PROFILE` | No | Name of a [configuration profile](https://platform.claude.com/docs/en/manage-claude/wif-reference#profile-configuration-file) to load. Takes precedence over the federation environment variables in this table. | `staging-profile` | The direct environment-variable federation path activates only when `ANTHROPIC_FEDERATION_RULE_ID`, `ANTHROPIC_ORGANIZATION_ID`, `ANTHROPIC_SERVICE_ACCOUNT_ID`, and one of `ANTHROPIC_IDENTITY_TOKEN_FILE` or `ANTHROPIC_IDENTITY_TOKEN` are all set. `ANTHROPIC_WORKSPACE_ID` is read alongside but does not gate activation. A variable that is set to an empty string still occupies its slot in the credential precedence chain. If `ANTHROPIC_API_KEY=""` is exported, the SDK selects the API-key path with an empty key rather than falling through to federation. Unset unused credential variables rather than blanking them. ### Credential precedence The SDK resolves credentials in this order. The first source that yields a credential wins. | Order | Source | Notes | | ----- | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | 1 | Constructor argument (`api_key=`, `auth_token=`, `credentials=`) | Always overrides everything else. | | 2 | `ANTHROPIC_API_KEY` or `ANTHROPIC_AUTH_TOKEN` | Shadows federation entirely. Unset these when migrating from API keys. | | 3 | `ANTHROPIC_PROFILE` | Loads `/configs/.json`. A missing named profile is an error, not a fall-through. | | 4 | Federation environment variables | `ANTHROPIC_FEDERATION_RULE_ID` + `ANTHROPIC_ORGANIZATION_ID` + `ANTHROPIC_SERVICE_ACCOUNT_ID` + `ANTHROPIC_IDENTITY_TOKEN[_FILE]`. | | 5 | Active profile | Resolved from `/active_config`, falling back to a profile named `default`. | When a profile is loaded, environment variables fill any fields the profile omits but never override fields the profile sets explicitly. For example, `ANTHROPIC_WORKSPACE_ID` fills `workspace_id` only when the active profile does not set it. ## Profile configuration file A profile is a named configuration file that the SDK and the `ant` CLI both read. Profiles let you ship federation parameters with your container image or switch between environments without changing code. ### Configuration directory The SDK locates the configuration directory in this order: 1. `$ANTHROPIC_CONFIG_DIR` 2. `~/.config/anthropic` on Linux and macOS 3. `%APPDATA%\Anthropic` on Windows ### Active profile The active profile name resolves in this order: 1. `$ANTHROPIC_PROFILE` 2. The contents of `/active_config` (a one-line file written by `ant profile activate `) 3. The literal name `default` Claude Code and the Claude Agent SDK honor this same resolution order, so a federation profile configured here also authenticates those tools without additional setup. ### File layout | Path | Contents | Sensitivity | | ----------------------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------- | | `/configs/.json` | `version`, the `authentication` block, `organization_id`, `workspace_id`, and `base_url`. | Non-secret. Safe to commit or bake into an image. | | `/credentials/.json` | `version`, the cached `access_token`, `expires_at`, and (for interactive login) `refresh_token`. | Secret. Written by the SDK with mode `0600`. | Both the config file and the credentials file carry a top-level string `version` field in `major.minor` format (currently `"1.0"`). The SDK writes this field automatically so future releases can detect and migrate older formats; omit it when authoring a config by hand and the SDK treats the file as the current version. ### Federation profile example ```json configs/production.json { "version": "1.0", "authentication": { "type": "oidc_federation", "federation_rule_id": "fdrl_...", "service_account_id": "svac_...", "identity_token": { "source": "file", "path": "/var/run/secrets/anthropic.com/token" } }, "organization_id": "00000000-0000-0000-0000-000000000000", "workspace_id": "wrkspc_...", "base_url": "https://api.anthropic.com" } ``` If `authentication.identity_token` is omitted, the SDK falls back to `ANTHROPIC_IDENTITY_TOKEN_FILE` or `ANTHROPIC_IDENTITY_TOKEN` from the environment. ## OAuth scopes The `oauth_scope` you set on a federation rule determines which Claude API endpoints the minted access token can call. | Scope | Grants access to | | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `workspace:developer` | All non-administrative Claude API endpoints in the rule's workspace: [Messages](https://platform.claude.com/docs/en/api/messages) (including streaming and token counting), [Models](https://platform.claude.com/docs/en/api/models-list), [Managed Agents](https://platform.claude.com/docs/en/managed-agents/overview) and their sessions, [Files](https://platform.claude.com/docs/en/build-with-claude/files), and [Skills](https://platform.claude.com/docs/en/build-with-claude/skills-guide). This matches the access an API key issued for the same workspace has. | | `workspace:inference` | The inference endpoints in the rule's workspace: [Messages](https://platform.claude.com/docs/en/api/messages) (including streaming and token counting), [Models](https://platform.claude.com/docs/en/api/models-list), and the [OpenAI-compatible chat endpoint](https://platform.claude.com/docs/en/cli-sdks-libraries/libraries/openai-sdk). Use this for workloads that only need to call Claude and never need to manage Files, Skills, or other resources. | | `workspace:manage_tunnels` | The [MCP tunnels API](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/reference#tunnels-api): create, list, and get tunnels, register and archive CA certificates, reveal and rotate the tunnel token, and archive tunnels. The Console's create-tunnel modal window locks this scope when you create a rule from it. | | `org:admin` | Full access to the [Admin API](https://platform.claude.com/docs/en/manage-claude/admin-api) (organization members, invites, workspaces, API keys, and the rest). An OAuth `org:admin` token can only create or modify rules scoped to `workspace:developer` or `workspace:inference`, and cannot update an issuer that backs a rule with any other scope; see the [constraints](https://platform.claude.com/docs/en/manage-claude/wif-admin-api#permissions-and-constraints). | A request to an endpoint outside the token's scope returns HTTP 403. Finer-grained scopes (per resource, or read versus write) are not currently available. ### Permission boundaries A federation rule's `oauth_scope` is a ceiling: the minted token can never exceed it. The target service account's `organization_role` (`developer` or `admin`) determines which scopes are grantable, so a rule that grants `org:admin` must target a service account with `organization_role=admin`. Effective permissions are the intersection of the rule's scope and the service account's role. | Rule `oauth_scope` | Service account `organization_role` | Effective permissions | | --------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `workspace:developer` | `admin` | Claude API access in the rule's workspace only. The scope caps the token below the role. | | `org:admin` | `admin` | Full Admin API access (organization members, invites, workspaces, API keys, and the rest), minus the OAuth-caller carve-outs; see [constraints](https://platform.claude.com/docs/en/manage-claude/wif-admin-api#permissions-and-constraints). | ## Validation rules Anthropic enforces these constraints when you create or update issuers and rules, and when verifying an incoming JWT at exchange time. For complete parameter details and response schemas, see the [Service accounts API reference](https://platform.claude.com/docs/en/api/admin/service_accounts), [Federation issuers API reference](https://platform.claude.com/docs/en/api/admin/federation_issuers), and [Federation rules API reference](https://platform.claude.com/docs/en/api/admin/federation_rules). ### Resource fields | Field | Constraint | | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Issuer, rule, and service account `name` | Must match `^[a-z0-9-]+$`, length 1 to 255 characters. | | `workspace_id` | Required on create unless `applies_to_all_workspaces` is true. The workspace (`wrkspc_...`) whose quota, billing, and rate limits apply to tokens minted under this rule. Must be a workspace in the same organization, and the target service account must be a member of that workspace. | | `applies_to_all_workspaces` | Boolean. Set `true` to enable the rule in every workspace in the organization instead of naming one; either this or `workspace_id` is required on create. | | `token_lifetime_seconds` | Integer between `60` and `86400` (1 minute to 24 hours). Default `3600`. Values outside this range are rejected at request time. See [Token lifetime and refresh](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation#token-lifetime-and-refresh). | ### URL fields The `issuer_url`, `jwks.discovery_base`, and `jwks.url` fields are validated: | Constraint | Detail | | ---------- | ------------------------------------------------------------------------------------------------------------------------ | | Scheme | Must be `https`. | | Port | Must be `443` (explicit or default). | | Host | Must be a public DNS hostname for your OIDC provider. Must resolve to public IP addresses; IP literals are not accepted. | URL validation failures return `400 invalid_request_error` with the field name as a prefix on the error message (for example, `issuer_url: url must use https scheme`). URL constraints apply only to URLs that Anthropic dials. In `explicit_url` and `inline` JWKS modes, and in `discovery` mode when `jwks.discovery_base` is set, the `issuer_url` is compared against the JWT `iss` claim as a string and is never fetched, so it may reference an internal hostname or non-standard port. ### JWT verification | Constraint | Detail | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Maximum size | The `assertion` JWT must be at most 16 KiB. | | Signing algorithm | Only asymmetric algorithms (RSA and ECDSA families: ES256, ES384, ES512, RS256, RS384, RS512, PS256, PS384, PS512) are accepted. HMAC (`HS256`, `HS384`, `HS512`) and `none` are rejected. | | Key ID | The JWT header must carry a `kid` that matches a key in the issuer's JWKS. Tokens without `kid` are rejected. | | Required claims | `sub` must be present. `iat` must be present and not in the future. `exp` must be present and in the future. | | Maximum lifetime | The token's lifetime (`exp` minus `iat`) must not exceed the issuer's configured maximum (1 hour by default, configurable for each issuer in the Claude Console). | | Clock skew | A 30-second leeway is applied to `exp`, `nbf`, and `iat`. | ## Rule matching semantics A federation rule's `match` block determines whether an incoming JWT is accepted. All populated fields are evaluated with AND semantics: the JWT must satisfy every populated matcher. At least one of `subject_prefix`, `claims`, or `condition` must be set; a `match` block that contains only `audience` (or no matchers at all) is rejected. This guards against rules that would accept every token from an issuer. | Matcher | Type | Semantics | | ---------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `subject_prefix` | string | Exact match against the JWT `sub` claim. A trailing `*` makes it a prefix match (the `sub` value must begin with the characters before the `*`). Case-sensitive. | | `audience` | string | The JWT `aud` claim must contain this exact string. When `aud` is an array, any element matching exactly satisfies the check. | | `claims` | map\ | Each key is a top-level claim name and each value is the required exact string value. For nested, numeric, boolean, or complex claims like lists and maps, use `condition` with a CEL expression instead. | | `condition` | string (CEL) | A [CEL](https://cel.dev/) expression that must evaluate to `true`. | ### CEL evaluation environment The `condition` expression has access to a single variable: | Variable | Type | Contents | | -------- | ---- | ----------------------------------------------------------------------------- | | `claims` | map | The full decoded JWT claim set. Nested objects are accessible as nested maps. | Example: ```text wrap claims.sub.startsWith("repo:acme-corp/") && claims.ref in ["refs/heads/main", "refs/heads/release"] ``` CEL conditions are security boundaries. An expression that evaluates to `true` for more inputs than intended grants broader access than intended. Prefer the static matchers when they express your constraint. ## Errors ### Token exchange errors `POST /v1/oauth/token` returns errors in the standard [API error shape](https://platform.claude.com/docs/en/api/errors). The SDK wraps exchange failures in a typed `FederationExchangeError` (or language equivalent) that exposes the HTTP status, the response body, and the `request_id`. | Status | Error | Cause | Resolution | | ------ | ----------------- | -------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 400 | `invalid_request` | `federation_rule_id` is malformed or a required request field is missing. | Verify the `fdrl_` ID and that the request body includes all required fields. | | 400 | `invalid_request` | `workspace_id_required`: the federation rule is enabled for more than one workspace and the request omits `workspace_id`. | Set `ANTHROPIC_WORKSPACE_ID` (or the `workspace_id` body field on a raw request) to the `wrkspc_...` ID you want the token scoped to. See [Token exchange request](https://platform.claude.com/docs/en/manage-claude/wif-reference#token-exchange-request). | | 400 | `invalid_grant` | The JWT `iss` claim does not equal the registered `issuer_url` exactly. | Compare byte-for-byte, including trailing slashes and scheme: `jq -rR 'split(".")[1] \| gsub("-";"+") \| gsub("_";"/") \| @base64d \| fromjson \| .iss' <<< "$JWT"`. | | 400 | `invalid_grant` | JWKS fetch failed, JWKS is stale, or the JWT was signed with a key not in the JWKS. | For `inline` mode, update the issuer with the rotated keys. For `discovery` and `explicit_url`, confirm the JWKS endpoint is reachable on port 443; if the issuer recently rotated its signing key, see [Key rotation and caching](https://platform.claude.com/docs/en/manage-claude/wif-reference#key-rotation-and-caching). | | 400 | `invalid_grant` | The JWT `exp` claim is in the past (beyond the 30-second skew window). | Confirm your identity provider is projecting a fresh token and the SDK is re-reading the token file. | | 400 | `invalid_grant` | The JWT was verified but its claims do not satisfy the rule's `match` block. | Decode the JWT and compare each claim against the rule. `subject_prefix` is case-sensitive. `audience` requires an exact element match. | | 400 | `invalid_grant` | The `federation_rule_id` does not exist, is archived, or the JWT is not authorized for it (consolidated to prevent enumeration). | Confirm the rule ID in the Claude Console and that the rule has not been archived. | All `invalid_grant` failures return HTTP 400; the specific cause is logged server-side only and not exposed in the response. ### Common SDK-side failures | Symptom | Cause | Resolution | | ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | SDK reports "no credentials" instead of exchanging | One of `ANTHROPIC_FEDERATION_RULE_ID`, `ANTHROPIC_ORGANIZATION_ID`, `ANTHROPIC_SERVICE_ACCOUNT_ID`, or `ANTHROPIC_IDENTITY_TOKEN[_FILE]` is unset and no profile is active. | Set all four variables, or configure a profile. | | SDK authenticates with an API key instead of federating | `ANTHROPIC_API_KEY` or `ANTHROPIC_AUTH_TOKEN` is set and wins precedence. | Unset the key or token variable. | | `FileNotFoundError` on first request | The path in `ANTHROPIC_IDENTITY_TOKEN_FILE` does not exist. The SDK opens the file lazily at exchange time. | Confirm the projected-token volume is mounted and the path matches. | | Token exchange succeeds but a Claude API request returns 403 | The minted token's scope does not grant access to that endpoint. | Check the rule's `oauth_scope` against [OAuth scopes](https://platform.claude.com/docs/en/manage-claude/wif-reference#oauth-scopes). | | Authentication fails with empty credential | A credential environment variable is exported but set to an empty string. Empty values still win their precedence slot. | Unset the variable with `unset VAR` rather than `VAR=""`. | ## Troubleshoot a failed exchange A `400 invalid_grant` response is intentionally opaque; the specific cause is logged server-side only. Start with the [authentication history page](https://platform.claude.com/settings/workload-identity-federation?tab=history) in the Claude Console. Recent exchange attempts surface the issuer and rule that were evaluated, the JWT claims that were inspected, and which validation step failed, which usually short-circuits the following checks. If you still need to debug from the JWT itself, work through these checks in order: Decode the assertion you sent so you can compare each claim against your issuer and rule configuration: ```bash cURL jq -rR 'split(".")[1] | gsub("-";"+") | gsub("_";"/") | @base64d | fromjson' <<< "$JWT" ``` The decoded `iss` claim must equal the registered `issuer_url` byte for byte, including scheme, port, and any trailing slash. A mismatch on a single character fails verification. The decoded `aud` claim must contain the rule's `audience` value as an exact match. When `aud` is an array, one element must match exactly. Compare `sub` against the rule's `subject_prefix` (case-sensitive; a trailing `*` is a prefix match, anything else is exact). Compare every key in the rule's `claims` map against the same-named top-level claim. `exp` must be in the future and `nbf`/`iat` must be in the past, within the 30-second skew window. If the workload host's clock has drifted, an otherwise valid token is rejected. For `discovery` mode, fetch `/.well-known/openid-configuration` over public HTTPS on port 443 and confirm `jwks_uri` resolves. For `explicit_url`, fetch the JWKS URL directly. For `inline`, confirm the issuer's signing key has not rotated since you registered the keys. If the issuer rotated its signing key and immediately started signing with it, exchanges can fail for up to a minute while Anthropic's JWKS cache refreshes. See [Key rotation and caching](https://platform.claude.com/docs/en/manage-claude/wif-reference#key-rotation-and-caching). ## JWKS source modes When you register a federation issuer, the `jwks` field controls how Anthropic obtains the public keys used to verify JWT signatures from that issuer. It is a discriminated union keyed on `type`: | `jwks.type` | `jwks` shape | Behavior | Use when | | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `discovery` (default) | `{ "type": "discovery", "discovery_base": "https://..." }` (`discovery_base` is optional; set it when the discovery URL differs from `issuer_url`) | Anthropic fetches `/.well-known/openid-configuration`, reads `jwks_uri` from the discovery document, and fetches the JWKS from there. | Your IdP serves a standard OIDC discovery document on the public internet. Most managed providers (EKS, GKE, Cloud Run, GitHub Actions, Entra ID) support this. | | `explicit_url` | `{ "type": "explicit_url", "url": "https://..." }` | Anthropic fetches the JWKS directly from `url`. The `issuer_url` is used only for string comparison against the JWT `iss` claim and is never dialed. | Your IdP does not serve a discovery document, or discovery is internal-only but the JWKS is publicly reachable. | | `inline` | `{ "type": "inline", "keys": [...] }` | You supply the array of JWK objects inline (the `keys` array from the JWKS document, not the wrapper object). Anthropic makes no outbound request. The `issuer_url` is used only for `iss` comparison. | Air-gapped environments, self-managed Kubernetes clusters with cluster-internal issuer URLs, or when you want explicit control over key rotation. | The discriminated union makes the companion fields mutually exclusive by construction. Both `discovery` and `explicit_url` also accept an optional `ca_cert_pem` string for issuers that serve TLS from a private CA. ### Key rotation and caching In `discovery` and `explicit_url` modes, Anthropic caches the fetched JWKS. If your identity provider publishes a new signing key and immediately starts signing tokens with it, exchanges that present those tokens may fail with a signature error for up to 1 minute while the cache refreshes. To avoid this window, publish a new signing key in the JWKS at least 15 minutes before your identity provider starts signing tokens with it, and keep the superseded key in the JWKS until tokens it signed have expired. Managed identity providers typically follow this discipline on their own. If you operate your own issuer (a self-managed Kubernetes cluster, a SPIRE OIDC discovery provider, or an Okta custom authorization server with a configured rotation cadence), confirm that your rotation policy publishes new keys ahead of first use. In `inline` mode there is no automatic key refresh. When your identity provider rotates its signing keys, you must update the issuer configuration with the new JWKS or all token exchanges will fail signature verification. --- title: Workload Identity Federation url: https://platform.claude.com/docs/en/manage-claude/workload-identity-federation description: Authenticate workloads to the Claude API with short-lived identity tokens from your own identity provider instead of long-lived static API keys. --- Workload Identity Federation (WIF) lets your workloads authenticate to the Claude API with short-lived OpenID Connect (OIDC) tokens instead of long-lived `sk-ant-...` API keys. The tokens come from an identity provider (IdP) you already operate: AWS IAM, Google Cloud, or any standards-compliant OIDC issuer such as GitHub Actions, Kubernetes, SPIFFE, Microsoft Entra ID, or Okta. Your workload presents a signed JWT from your identity provider. Anthropic validates it against trust rules you configure in the Claude Console and returns a short-lived Anthropic access token bound to a service account in your organization. There are no static secrets to mint, store in CI, rotate, or leak. Workload Identity Federation strengthens your security posture by replacing static API keys with tokens that expire in minutes rather than never. It is not a complete security story on its own: federated authentication is only as strong as the upstream identity provider that signs the JWT. Pair Workload Identity Federation with the controls your IdP already supports (workload identity binding, conditional access, audit logging) for defense in depth. ## Concepts You configure three resources in the Claude Console before any workload can federate. Together they express "tokens signed by issuer X, with claims that look like Y, may act as service account Z." ### Service accounts A **service account** (`svac_...`) is a named, non-human identity inside your Anthropic organization. It is the principal that a federated token acts as. Service accounts live at the organization level and become active in a workspace when you add them as members of that workspace. At exchange time, Anthropic checks that the federation rule's workspace matches one of the service account's workspace memberships; the minted token then follows that workspace's rate limits and usage attribution, the same as an API key. Unlike a human user, a service account has no email, no password, and no Console login. Every service account is implicitly a member of your organization's default workspace; add explicit memberships for any other workspace it should act in. The key distinction from an API key: an API key *is* a credential, while a service account *has* credentials minted for it on demand. You can audit which workloads acted as which service account. ### Federation issuers A **federation issuer** (`fdis_...`) registers an OIDC identity provider with your organization. Registering an issuer tells Anthropic "JWTs signed by this provider may assert workload identity for my org." An issuer has two pieces of configuration: * **Issuer URL:** The exact `iss` claim value that appears in the provider's JWTs, for example `https://token.actions.githubusercontent.com` or `https://oidc.eks.us-west-2.amazonaws.com/id/EXAMPLE`. * **JWKS source:** How Anthropic fetches the public keys to verify JWT signatures. Use `discovery` (the default) for any provider that serves `/.well-known/openid-configuration` at its issuer URL. Use `explicit_url` to point at a JWKS endpoint directly, or `inline` to upload the key set for issuers that are not reachable from the public internet (for example, a private Kubernetes cluster). Issuer and JWKS URLs must be `https`, on port 443, and use a public DNS hostname that resolves to public IP addresses; IP literals are not accepted. These constraints apply only to URLs Anthropic fetches; in `explicit_url` and `inline` modes the `issuer_url` is compared as a string and may reference an internal hostname. You typically register one issuer per environment: your production EKS cluster, your staging cluster, and GitHub Actions are three separate issuers. ### Federation rules A **federation rule** (`fdrl_...`) is the bridge between an issuer and a service account: "when a JWT from issuer X has claims that look like Y, mint a token for service account Z with scope S." A rule defines match conditions, a target, and the authorization scope and token lifetime that apply when the rule matches: * **Match:** The conditions an incoming JWT must satisfy. You can match on a `subject_prefix` (for example, `system:serviceaccount:prod:worker`, or with a trailing `*` for a prefix match), an exact `audience`, a map of exact claim values, a [CEL](https://cel.dev/) `condition` expression for complex logic, or any combination. At least one of `subject_prefix`, `claims`, or `condition` must be set, and all configured matchers must pass for the JWT to be accepted. * **Target:** The service account the matched JWT maps to. * **Authorization:** The OAuth `scope` granted on the minted token. The default is `workspace:developer`, which grants the same access as an API key issued for that workspace. Some products lock the scope when you create a rule from their flow; for example, the [MCP tunnels](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/overview) create-tunnel modal creates rules scoped to `workspace:manage_tunnels`. See [OAuth scopes](https://platform.claude.com/docs/en/manage-claude/wif-reference#oauth-scopes). The rule also sets `token_lifetime_seconds` (60 to 86400, default 3600). A single issuer can have many rules: one per team, namespace, or permission level. Rules are evaluated by ID: the client specifies which rule to use in the exchange request, and Anthropic verifies the JWT satisfies that rule's match criteria. There is no implicit rule search. ## How it works 1. **Your IdP issues a JWT to the workload.** On most platforms this is ambient: a Kubernetes projected service-account token, the Google Cloud metadata server, Azure IMDS, or the GitHub Actions OIDC endpoint. The JWT's `iss` claim identifies the provider, and its `sub` and other claims identify the specific workload. 2. **The SDK exchanges the JWT for an Anthropic access token.** The SDK posts the JWT to `POST /v1/oauth/token` using the [RFC 7523](https://www.rfc-editor.org/rfc/rfc7523) `jwt-bearer` grant. Anthropic verifies the JWT against the issuer's JWKS and the federation rule's match conditions, then returns a short-lived `sk-ant-oat01-...` token that acts on behalf of the rule's target service account. 3. **The SDK sends the token on every request and refreshes it before it expires.** Your application code constructs the client with no `api_key` and calls the API as usual. The SDK re-runs the exchange before the token expires. ## Set up federation You need the admin, owner, or primary owner role in your Anthropic organization, an OIDC-capable identity provider with a reachable JWKS endpoint (or a JWKS document you can paste, for air-gapped clusters), and a workload that can obtain an identity token from that provider. The **Connect workload** wizard creates all three resources (the issuer, the service account, and the federation rule) in one guided flow, then verifies the connection end to end. In the Claude Console, go to **Settings → Workload identity** and select **Connect workload**. Select the tile for your identity provider: GitHub Actions, AWS, Google Cloud, Microsoft Entra ID, or Kubernetes. Each tile prefills the issuer URL pattern and the match fields that provider's JWTs support. For any other standards-compliant provider (such as SPIFFE or Okta), select **Custom OIDC**. The wizard walks you through the provider-specific fields: the issuer configuration, the match conditions for incoming JWTs, and names for the service account and federation rule it creates. The wizard prefills `oauth_scope=workspace:developer` and `token_lifetime_seconds=600` (the API default when `token_lifetime_seconds` is omitted is 3600); adjust these if your workload needs a different scope or lifetime. Optionally select **Verify issuer** to dry-run the issuer configuration before anything is created. Verification confirms Anthropic can fetch and parse the JWKS from the URLs you entered, which catches reachability and configuration mistakes early. The wizard creates the issuer, service account, and federation rule, then listens for a successful token exchange for 15 minutes. Trigger an exchange from your workload within that window (see [Authenticate from your workload](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation#authenticate-from-your-workload)) to confirm the setup works. If the window elapses, the resources persist; you can re-run the test from the federation rule's detail page. Note the rule's ID (`fdrl_...`) and the service account ID (`svac_...`) the wizard creates: your workload passes both, along with your organization ID (and your workspace ID when the rule covers more than one workspace), in every token-exchange request. To manage these resources programmatically, see [Manage WIF with the Admin API](https://platform.claude.com/docs/en/manage-claude/wif-admin-api) for the curl walkthrough, or see the [Service accounts API reference](https://platform.claude.com/docs/en/api/admin/service_accounts), [Federation issuers API reference](https://platform.claude.com/docs/en/api/admin/federation_issuers), and [Federation rules API reference](https://platform.claude.com/docs/en/api/admin/federation_rules) for complete parameter details and response schemas. ## Authenticate from your workload With federation configured, your workload exchanges its IdP-issued JWT for an Anthropic token at runtime. The SDKs handle the exchange and refresh loop for you. The cURL tab shows the underlying HTTP exchange for shell scripts, debugging, or languages without SDK support. ### Construct the SDK client You can construct the client with explicit credentials or with no arguments. With no arguments, the SDK resolves credentials from environment variables or the active profile, as described under [Credential precedence](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation#credential-precedence). The zero-argument form is the recommended pattern for production workloads: ship the same container image everywhere and inject `ANTHROPIC_FEDERATION_RULE_ID`, `ANTHROPIC_ORGANIZATION_ID`, `ANTHROPIC_SERVICE_ACCOUNT_ID`, `ANTHROPIC_WORKSPACE_ID`, and `ANTHROPIC_IDENTITY_TOKEN_FILE` per environment. ```bash cURL # 1. Acquire your IdP's JWT (platform-specific; see the per-provider guides). JWT=$(cat /var/run/secrets/anthropic.com/token) # 2. Exchange it for a short-lived Anthropic access token. RESPONSE=$(curl -sS https://api.anthropic.com/v1/oauth/token \ -H "content-type: application/json" \ -d @- <messages->create( model: 'claude-opus-5', maxTokens: 1024, messages: [['role' => 'user', 'content' => 'Hello, Claude']], ); $textBlock = array_find($message->content, static fn ($block): bool => $block->type === 'text'); echo $textBlock->text . PHP_EOL; ``` ```ruby Ruby client = Anthropic::Client.new( credentials: Anthropic::Credentials::WorkloadIdentity.new( identity_token_provider: Anthropic::Credentials::IdentityTokenFile.new( "/var/run/secrets/anthropic.com/token" ), federation_rule_id: "fdrl_...", organization_id: "00000000-0000-0000-0000-000000000000", service_account_id: "svac_...", workspace_id: "wrkspc_..." ) ) message = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [{role: "user", content: "Hello, Claude"}] ) puts message.content.find { it.type == :text }.text ``` The token-exchange response follows [RFC 6749 §5.1](https://www.rfc-editor.org/rfc/rfc6749#section-5.1). See [Token exchange response](https://platform.claude.com/docs/en/manage-claude/wif-reference#token-exchange-response) for the field reference. ## Credential precedence Every SDK resolves credentials in the same five-tier order: constructor arguments, then `ANTHROPIC_API_KEY` / `ANTHROPIC_AUTH_TOKEN`, then an explicit `ANTHROPIC_PROFILE`, then the federation environment variables, then the implicit active profile. The first source that yields a credential wins. `ANTHROPIC_API_KEY` sits above the federation tiers, so a leftover key in the environment silently shadows federation. When migrating a workload from API keys to Workload Identity Federation, confirm `ANTHROPIC_API_KEY` is unset everywhere that workload runs (container env, CI secrets, shell profiles). The CLI's [`ant auth status`](https://platform.claude.com/docs/en/cli-sdks-libraries/cli/authentication#check-authentication-status) command reports which source won. For the full precedence table, the per-tier semantics, and the profile file schema, see [Credential precedence in the WIF reference](https://platform.claude.com/docs/en/manage-claude/wif-reference#credential-precedence). ## Migrate from API keys To switch an existing workload from a static API key to federation without downtime: 1. **Configure federation in parallel.** Complete the [setup walkthrough](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation#set-up-federation) and confirm the federation rule matches your workload's token. Leave the existing `ANTHROPIC_API_KEY` in place for now. 2. **Smoke-test which credential wins.** Run `ant auth status` from inside the workload (or inspect SDK debug logs). Because `ANTHROPIC_API_KEY` sits above the federation tiers in the precedence chain, the API key still wins at this stage. 3. **Unset `ANTHROPIC_API_KEY` everywhere it is injected.** Remove it from CI secrets, container environment, and shell profiles (see the preceding warning). Re-run `ant auth status` and confirm the federation source is now selected. 4. **Revoke the API key.** Once the workload is running on the federated token, delete the key in the Claude Console under **Settings → API keys**. ## Token lifetime and refresh The minted Anthropic token's lifetime is the lesser of (a) the rule's `token_lifetime_seconds` (default 3,600 seconds) and (b) twice the remaining lifetime of the IdP JWT you presented. The result is never less than 60 seconds. The second bound prevents an Anthropic token from outliving the upstream identity it was derived from by more than a small margin. The SDKs cache the token and refresh it on a two-tier schedule modeled on `botocore`: * **Advisory refresh** at expiry minus 120 seconds. The SDK attempts a new exchange. If the token endpoint is unreachable, the SDK continues serving the cached token, which is still valid for roughly 90 more seconds. * **Mandatory refresh** at expiry minus 30 seconds. A failed exchange at this point raises an error. The cached token is too close to expiry to be safe. Because the SDK re-reads `ANTHROPIC_IDENTITY_TOKEN_FILE` on every exchange, it transparently picks up rotated projected tokens (Kubernetes service-account tokens, for example, rotate well before their `exp`). ## Identity providers Each guide covers where the JWT comes from on that platform, what its claims look like, and the issuer and rule configuration to register. STS web identity tokens, or EKS IRSA projected tokens. Google-signed identity tokens from the metadata server. Managed Identity (IMDS) and Entra Workload ID on AKS. Keyless CI authentication with the Actions OIDC token. Self-managed and on-premises clusters using projected service-account tokens. Workloads with SPIFFE JWT-SVIDs from SPIRE or another conformant issuer. Okta service applications using client-credentials flow. ## See also * [Manage WIF with the Admin API](https://platform.claude.com/docs/en/manage-claude/wif-admin-api): create issuers, service accounts, and rules from infrastructure as code * [WIF reference](https://platform.claude.com/docs/en/manage-claude/wif-reference): environment variables, profile file schema, validation rules, and error codes * [Authentication](https://platform.claude.com/docs/en/manage-claude/authentication): all authentication options across the Anthropic SDKs * [Admin API reference](https://platform.claude.com/docs/en/api/admin): generated request and response schemas for every Admin API endpoint ### Authentication > Identity providers --- title: Use WIF with AWS url: https://platform.claude.com/docs/en/manage-claude/wif-providers/aws description: Authenticate AWS workloads on Lambda, EC2, ECS, or EKS to the Claude API with Workload Identity Federation and STS-issued identity tokens. --- AWS workloads can authenticate to the Claude API without static API keys by exchanging an AWS-signed OIDC identity token. The recommended path calls the AWS STS [`GetWebIdentityToken`](https://docs.aws.amazon.com/STS/latest/APIReference/API_GetWebIdentityToken.html) API, which works anywhere the workload has AWS credentials: Lambda, EC2, ECS, and EKS. EKS workloads can alternatively use the [Kubernetes projected-token path](https://platform.claude.com/docs/en/manage-claude/wif-providers/aws#use-eks-projected-service-account-tokens), which has fewer configuration steps but only works inside a pod. This guide shows both paths. For the underlying concepts (service accounts, federation issuers, and federation rules), see [Workload Identity Federation](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation). ## Prerequisites * Familiarity with [WIF concepts](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation#concepts): service accounts, federation issuers, and federation rules. * An AWS workload (EKS pod, ECS task, Lambda function, or EC2 instance) with an attached IAM role. * The `aws` CLI or an AWS SDK available in the workload. * Permission to create service accounts, federation issuers, and federation rules in the Claude Console for your Anthropic organization. ## Use STS web identity tokens (recommended) The AWS STS `GetWebIdentityToken` API returns an OIDC token signed by AWS that asserts the caller's IAM identity. Because it uses the workload's ambient AWS credentials, the same integration covers Lambda, EC2, ECS, and EKS. ### Configure AWS This is an account-level flag, off by default. In the AWS console, open **IAM**, choose **Account settings**, and enable **Outbound web identity federation**. To enable it programmatically: ```bash python3 -c "import boto3; boto3.client('iam').enable_outbound_web_identity_federation()" ``` If this is not enabled, calls to `GetWebIdentityToken` fail with `OutboundWebIdentityFederationDisabledException`. Attach this policy to the IAM role that your Lambda function, EC2 instance, or ECS task runs as: ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["sts:GetWebIdentityToken"], "Resource": "*" } ] } ``` After enabling outbound federation, the **IAM > Account settings** page shows a **Get Token Issuer URL** field with a value of the form `https://.tokens.sts.global.api.aws`. This URL is unique to your AWS account; copy it for the next step. To retrieve it programmatically: ```bash python3 -c "import boto3; print(boto3.client('iam').get_outbound_web_identity_federation_info())" ``` ### Configure Anthropic In the Claude Console, open **Settings → Workload identity**, click **Connect workload**, and select the **AWS** tile. The wizard walks you through registering the issuer, creating a service account, and creating a federation rule. The wizard creates these resources for you. Use the following values whether you enter them in the wizard or send them to the [Admin API](https://platform.claude.com/docs/en/manage-claude/wif-admin-api): **Federation issuer:** Register the per-account STS issuer URL you copied in the prior step. It exposes a public JWKS endpoint, so use discovery mode. ```json { "name": "aws-sts", "issuer_url": "https://.tokens.sts.global.api.aws", "jwks": { "type": "discovery" } } ``` **Federation rule:** Match the audience you pass to `GetWebIdentityToken` and the calling role's IAM role ARN in the `sub` claim. The `sub` value is the IAM role ARN of the workload that called the API, in the form `arn:aws:iam:::role/`. The token also carries an `https://sts.amazonaws.com/` claim with `aws_account`, `org_id`, `principal_id`, and any `request_tags` you passed; you can match on those with the rule's `claims` map or a CEL `condition` for finer control. ```json { "name": "prod-inference", "issuer_id": "fdis_...", "match": { "subject_prefix": "arn:aws:iam::123456789012:role/inference-worker", "audience": "https://api.anthropic.com" }, "target": { "type": "service_account", "service_account_id": "svac_..." }, "workspace_id": "wrkspc_...", "oauth_scope": "workspace:developer", "token_lifetime_seconds": 600 } ``` Be as specific as the workload allows. Match the exact role ARN, and only broaden `subject_prefix` (for example, to `arn:aws:iam::123456789012:role/*`) if multiple IAM roles should map to the same Anthropic service account. ### Acquire and use the token Call `GetWebIdentityToken` with `https://api.anthropic.com` as the audience, then pass the result to the SDK's federation credentials. The token provider is a callable, so the SDK re-invokes STS on each refresh. `GetWebIdentityToken` is available only on regional STS endpoints. If you receive `'STS' object has no attribute 'get_web_identity_token'` or a similar error, pin your STS client to a region (for example, `boto3.client("sts", region_name="us-east-1")`) and ensure your AWS SDK is recent enough to include the API. ```bash cURL JWT=$(aws sts get-web-identity-token \ --region us-east-1 \ --audience "https://api.anthropic.com" \ --signing-algorithm RS256 \ --duration-seconds 900 \ --query WebIdentityToken --output text) RESPONSE=$(curl -sS https://api.anthropic.com/v1/oauth/token \ -H "content-type: application/json" \ --data @- < str: sts = boto3.client("sts", region_name="us-east-1") resp = sts.get_web_identity_token( Audience=["https://api.anthropic.com"], SigningAlgorithm="RS256", DurationSeconds=900, ) return resp["WebIdentityToken"] client = anthropic.Anthropic( credentials=WorkloadIdentityCredentials( identity_token_provider=get_sts_web_identity_token, federation_rule_id=os.environ["ANTHROPIC_FEDERATION_RULE_ID"], organization_id=os.environ["ANTHROPIC_ORGANIZATION_ID"], service_account_id=os.environ["ANTHROPIC_SERVICE_ACCOUNT_ID"], workspace_id=os.environ.get("ANTHROPIC_WORKSPACE_ID"), ), ) message = client.messages.create( model="claude-opus-5", max_tokens=1024, messages=[{"role": "user", "content": "Hello from AWS"}], ) print(next(block.text for block in message.content if block.type == "text")) ``` ```typescript TypeScript import Anthropic from "@anthropic-ai/sdk"; import { oidcFederationProvider } from "@anthropic-ai/sdk/lib/credentials/oidc-federation"; import { STSClient, GetWebIdentityTokenCommand } from "@aws-sdk/client-sts"; const sts = new STSClient({ region: "us-east-1" }); async function getStsWebIdentityToken(): Promise { const out = await sts.send( new GetWebIdentityTokenCommand({ Audience: ["https://api.anthropic.com"], SigningAlgorithm: "RS256", DurationSeconds: 900 }) ); return out.WebIdentityToken!; } const client = new Anthropic({ credentials: oidcFederationProvider({ identityTokenProvider: getStsWebIdentityToken, federationRuleId: process.env.ANTHROPIC_FEDERATION_RULE_ID!, organizationId: process.env.ANTHROPIC_ORGANIZATION_ID!, serviceAccountId: process.env.ANTHROPIC_SERVICE_ACCOUNT_ID, workspaceId: process.env.ANTHROPIC_WORKSPACE_ID, baseURL: "https://api.anthropic.com", fetch }) }); const message = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, messages: [{ role: "user", content: "Hello from AWS" }] }); for (const block of message.content) { if (block.type === "text") { console.log(block.text); } } ``` ```go Go ctx := context.TODO() cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion("us-east-1")) if err != nil { panic(err) } stsClient := sts.NewFromConfig(cfg) getStsToken := option.IdentityTokenFunc(func(ctx context.Context) (string, error) { out, err := stsClient.GetWebIdentityToken(ctx, &sts.GetWebIdentityTokenInput{ Audience: []string{"https://api.anthropic.com"}, SigningAlgorithm: "RS256", DurationSeconds: aws.Int32(900), }) if err != nil { return "", err } return *out.WebIdentityToken, nil }) client := anthropic.NewClient( option.WithFederationTokenProvider(getStsToken, option.FederationOptions{ FederationRuleID: os.Getenv("ANTHROPIC_FEDERATION_RULE_ID"), OrganizationID: os.Getenv("ANTHROPIC_ORGANIZATION_ID"), ServiceAccountID: os.Getenv("ANTHROPIC_SERVICE_ACCOUNT_ID"), WorkspaceID: os.Getenv("ANTHROPIC_WORKSPACE_ID"), }), ) message, err := client.Messages.New(ctx, anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Hello from AWS")), }, }) if err != nil { panic(err) } for _, block := range message.Content { if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok { fmt.Println(textBlock.Text) break } } ``` ```java Java StsClient sts = StsClient.builder().region(Region.US_EAST_1).build(); IdentityTokenProvider getStsToken = () -> sts.getWebIdentityToken( GetWebIdentityTokenRequest.builder() .audience("https://api.anthropic.com") .signingAlgorithm("RS256") .durationSeconds(900) .build()) .webIdentityToken(); AnthropicClient client = AnthropicOkHttpClient.builder() .federationTokenProvider( getStsToken, System.getenv("ANTHROPIC_FEDERATION_RULE_ID"), System.getenv("ANTHROPIC_ORGANIZATION_ID"), System.getenv("ANTHROPIC_SERVICE_ACCOUNT_ID")) .build(); var message = client.messages().create(MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .addUserMessage("Hello from AWS") .build()); IO.println(message.content()); ``` ```csharp C# var credentials = new WorkloadIdentityCredentials(new WorkloadIdentityOptions { FederationRuleId = Environment.GetEnvironmentVariable("ANTHROPIC_FEDERATION_RULE_ID")!, OrganizationId = Environment.GetEnvironmentVariable("ANTHROPIC_ORGANIZATION_ID"), ServiceAccountId = Environment.GetEnvironmentVariable("ANTHROPIC_SERVICE_ACCOUNT_ID"), WorkspaceId = Environment.GetEnvironmentVariable("ANTHROPIC_WORKSPACE_ID"), IdentityTokenProvider = new StsTokenProvider(), }); using var client = new AnthropicOidcClient(credentials); var message = await client.Messages.Create(new() { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [new() { Role = Role.User, Content = "Hello from AWS" }], }); foreach (var block in message.Content) { if (block.Value is TextBlock textBlock) { Console.WriteLine(textBlock.Text); } } class StsTokenProvider : IIdentityTokenProvider { private readonly AmazonSecurityTokenServiceClient _sts = new(Amazon.RegionEndpoint.USEast1); public async Task GetIdentityTokenAsync(CancellationToken ct = default) { var resp = await _sts.GetWebIdentityTokenAsync(new GetWebIdentityTokenRequest { Audience = ["https://api.anthropic.com"], SigningAlgorithm = "RS256", DurationSeconds = 900, }, ct); return resp.WebIdentityToken; } } ``` ```bash CLI TOKEN_FILE=$(mktemp) aws sts get-web-identity-token \ --region us-east-1 \ --audience "https://api.anthropic.com" \ --signing-algorithm RS256 \ --duration-seconds 900 \ --query WebIdentityToken --output text > "$TOKEN_FILE" export ANTHROPIC_IDENTITY_TOKEN_FILE="$TOKEN_FILE" # ANTHROPIC_FEDERATION_RULE_ID, ANTHROPIC_ORGANIZATION_ID, and # ANTHROPIC_SERVICE_ACCOUNT_ID, and ANTHROPIC_WORKSPACE_ID are read from the environment ant messages create \ --model claude-opus-5 \ --max-tokens 1024 \ --message '{role: user, content: "Hello from AWS"}' ``` ```php PHP use Anthropic\Client; use Anthropic\Credentials\WorkloadIdentityCredentials; use Aws\Sts\StsClient; $sts = new StsClient(['region' => 'us-east-1', 'version' => 'latest']); $client = new Client(credentials: new WorkloadIdentityCredentials( identityTokenProvider: fn() => $sts->getWebIdentityToken([ 'Audience' => ['https://api.anthropic.com'], 'SigningAlgorithm' => 'RS256', 'DurationSeconds' => 900, ])['WebIdentityToken'], federationRuleId: getenv('ANTHROPIC_FEDERATION_RULE_ID'), organizationId: getenv('ANTHROPIC_ORGANIZATION_ID'), serviceAccountId: getenv('ANTHROPIC_SERVICE_ACCOUNT_ID'), workspaceId: getenv('ANTHROPIC_WORKSPACE_ID') ?: null, )); $message = $client->messages->create( model: 'claude-opus-5', maxTokens: 1024, messages: [['role' => 'user', 'content' => 'Hello from AWS']], ); $textBlock = array_find($message->content, static fn ($block): bool => $block->type === 'text'); echo $textBlock->text, PHP_EOL; ``` ```ruby Ruby require "anthropic" require "aws-sdk-sts" sts = Aws::STS::Client.new(region: "us-east-1") client = Anthropic::Client.new( credentials: Anthropic::WorkloadIdentityCredentials.new( identity_token_provider: -> { sts.get_web_identity_token( audience: ["https://api.anthropic.com"], signing_algorithm: "RS256", duration_seconds: 900, ).web_identity_token }, federation_rule_id: ENV.fetch("ANTHROPIC_FEDERATION_RULE_ID"), organization_id: ENV.fetch("ANTHROPIC_ORGANIZATION_ID"), service_account_id: ENV.fetch("ANTHROPIC_SERVICE_ACCOUNT_ID"), workspace_id: ENV["ANTHROPIC_WORKSPACE_ID"], ), ) message = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [{role: "user", content: "Hello from AWS"}] ) puts message.content.find { it.type == :text }.text ``` ### Verify the setup From inside the workload, exchange an STS-issued token directly and inspect the response: ```bash cURL JWT=$(aws sts get-web-identity-token \ --region us-east-1 \ --audience "https://api.anthropic.com" \ --signing-algorithm RS256 \ --duration-seconds 900 \ --query WebIdentityToken --output text) curl -sS https://api.anthropic.com/v1/oauth/token \ -H "content-type: application/json" \ -d "{ \"grant_type\": \"urn:ietf:params:oauth:grant-type:jwt-bearer\", \"assertion\": \"$JWT\", \"federation_rule_id\": \"fdrl_...\", \"organization_id\": \"00000000-0000-0000-0000-000000000000\", \"service_account_id\": \"svac_...\", \"workspace_id\": \"wrkspc_...\" }" | jq ``` A successful exchange returns an `access_token` beginning with `sk-ant-oat01-` and an `expires_in` value in seconds. On `400 invalid_grant`, see [Troubleshoot a failed exchange](https://platform.claude.com/docs/en/manage-claude/wif-reference#troubleshoot-a-failed-exchange); the most common AWS-side cause is an `iss` mismatch (the per-account STS issuer URL must match the registered `issuer_url` exactly). ## Use EKS projected service-account tokens If your workload runs in an EKS pod, you can skip the STS call and read a Kubernetes-projected service-account token directly from disk. Kubernetes natively projects an OIDC-compatible token into the pod, and the SDK can read it from a file path, so no token-provider callable is required. This path has two fewer AWS configuration steps than the STS path but only works inside a pod; the underlying mechanism is the same as the [generic Kubernetes integration](https://platform.claude.com/docs/en/manage-claude/wif-providers/kubernetes). This path additionally requires an EKS cluster with an [IAM OIDC provider enabled](https://docs.aws.amazon.com/eks/latest/userguide/enable-iam-roles-for-service-accounts.html) and `kubectl` access to the cluster. ### Configure your EKS cluster Each EKS cluster has a unique OIDC issuer. Retrieve it with the AWS CLI: ```bash CLI aws eks describe-cluster \ --name \ --query "cluster.identity.oidc.issuer" \ --output text ``` The output looks like `https://oidc.eks.us-west-2.amazonaws.com/id/6FA42E7BFDE8549CB...`. You'll register this URL as a federation issuer in the next section. The EKS pod identity webhook detects the `eks.amazonaws.com/role-arn` annotation and automatically projects a token with `aud: sts.amazonaws.com`, exposing its path as `AWS_WEB_IDENTITY_TOKEN_FILE`. That token is for AWS role assumption. For the Anthropic exchange, project a second token with `audience: https://api.anthropic.com` and mount it at a dedicated path. ```yaml apiVersion: v1 kind: ServiceAccount metadata: name: inference-worker namespace: inference annotations: eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/inference-worker ``` ```yaml apiVersion: v1 kind: Pod metadata: name: inference-worker namespace: inference spec: serviceAccountName: inference-worker volumes: - name: anthropic-token projected: sources: - serviceAccountToken: audience: https://api.anthropic.com expirationSeconds: 3600 path: token containers: - name: app image: your-registry/inference-worker:latest env: - name: ANTHROPIC_IDENTITY_TOKEN_FILE value: /var/run/secrets/anthropic.com/token - name: ANTHROPIC_FEDERATION_RULE_ID value: fdrl_... - name: ANTHROPIC_ORGANIZATION_ID value: 00000000-0000-0000-0000-000000000000 - name: ANTHROPIC_SERVICE_ACCOUNT_ID value: svac_... - name: ANTHROPIC_WORKSPACE_ID # required when the rule covers multiple workspaces value: wrkspc_... volumeMounts: - name: anthropic-token mountPath: /var/run/secrets/anthropic.com readOnly: true ``` The projected token is a JSON Web Token (JWT) signed by your cluster's OIDC issuer. Its `sub` claim follows the Kubernetes convention `system:serviceaccount::`: ```json { "iss": "https://oidc.eks.us-west-2.amazonaws.com/id/6FA42E7BFDE8549CB...", "sub": "system:serviceaccount:inference:inference-worker", "aud": ["https://api.anthropic.com"], "kubernetes.io": { "namespace": "inference", "serviceaccount": { "name": "inference-worker", "uid": "..." } }, "exp": 1775527120, "iat": 1775523520 } ``` The `serviceAccountToken` projection sets `aud` to `https://api.anthropic.com`. The separate IRSA-injected token at `AWS_WEB_IDENTITY_TOKEN_FILE` carries `aud: sts.amazonaws.com` and is for AWS API calls, not this exchange. ### Configure Anthropic In the Claude Console, open **Settings → Workload identity**, click **Connect workload**, and select the **AWS** tile. The wizard walks you through registering the issuer, creating a service account, and creating a federation rule. The wizard creates these resources for you. Use the following values whether you enter them in the wizard or send them to the [Admin API](https://platform.claude.com/docs/en/manage-claude/wif-admin-api): **Federation issuer:** EKS issuers expose a public JWKS endpoint, so use discovery mode. The issuer URL must exactly match the token's `iss` claim. Register one issuer per cluster. ```json { "name": "prod-eks-uswest2", "issuer_url": "https://oidc.eks.us-west-2.amazonaws.com/id/6FA42E7BFDE8549CB...", "jwks": { "type": "discovery" } } ``` **Federation rule:** Match the Kubernetes `sub` claim and the Anthropic audience `https://api.anthropic.com`. (Project a dedicated service-account token with that audience; don't reuse the IRSA default `sts.amazonaws.com` token.) ```json { "name": "prod-inference", "issuer_id": "fdis_...", "match": { "subject_prefix": "system:serviceaccount:inference:inference-worker", "audience": "https://api.anthropic.com" }, "target": { "type": "service_account", "service_account_id": "svac_..." }, "workspace_id": "wrkspc_...", "oauth_scope": "workspace:developer", "token_lifetime_seconds": 600 } ``` Be as specific as the workload allows. Loosen `subject_prefix` to `system:serviceaccount:inference:*` (the trailing `*` makes it a prefix match) only if every service account in the namespace should map to the same Anthropic service account. ### Acquire and use the token Inside the pod, the projected token is at `/var/run/secrets/anthropic.com/token` (exposed as `ANTHROPIC_IDENTITY_TOKEN_FILE` in the Pod spec). Pass that file to the SDK's federation credentials and the SDK handles the exchange and refresh. ```bash cURL JWT=$(cat "$ANTHROPIC_IDENTITY_TOKEN_FILE") RESPONSE=$(curl -sS https://api.anthropic.com/v1/oauth/token \ -H "content-type: application/json" \ --data @- <messages->create( model: 'claude-opus-5', maxTokens: 1024, messages: [['role' => 'user', 'content' => 'Hello from EKS']], ); $textBlock = array_find($message->content, static fn ($block): bool => $block->type === 'text'); echo $textBlock->text, PHP_EOL; ``` ```ruby Ruby require "anthropic" # Reads ANTHROPIC_FEDERATION_RULE_ID, ANTHROPIC_ORGANIZATION_ID, # ANTHROPIC_SERVICE_ACCOUNT_ID, ANTHROPIC_WORKSPACE_ID, and ANTHROPIC_IDENTITY_TOKEN_FILE client = Anthropic::Client.new message = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [{role: "user", content: "Hello from EKS"}] ) puts message.content.find { it.type == :text }.text ``` The Pod spec already sets `ANTHROPIC_IDENTITY_TOKEN_FILE`, `ANTHROPIC_FEDERATION_RULE_ID`, `ANTHROPIC_ORGANIZATION_ID`, `ANTHROPIC_SERVICE_ACCOUNT_ID`, and `ANTHROPIC_WORKSPACE_ID`, so you can construct the client with no arguments and the SDK reads the federation environment variables automatically. ### Verify the setup From inside the pod, exchange the projected token directly and inspect the response: ```bash cURL JWT=$(cat "$ANTHROPIC_IDENTITY_TOKEN_FILE") curl -sS https://api.anthropic.com/v1/oauth/token \ -H "content-type: application/json" \ -d "{ \"grant_type\": \"urn:ietf:params:oauth:grant-type:jwt-bearer\", \"assertion\": \"$JWT\", \"federation_rule_id\": \"$ANTHROPIC_FEDERATION_RULE_ID\", \"organization_id\": \"$ANTHROPIC_ORGANIZATION_ID\", \"service_account_id\": \"$ANTHROPIC_SERVICE_ACCOUNT_ID\", \"workspace_id\": \"$ANTHROPIC_WORKSPACE_ID\" }" | jq ``` A successful exchange returns an `access_token` beginning with `sk-ant-oat01-` and an `expires_in` value in seconds. On `400 invalid_grant`, see [Troubleshoot a failed exchange](https://platform.claude.com/docs/en/manage-claude/wif-reference#troubleshoot-a-failed-exchange); the most common EKS-side cause is the projected token's `aud` not matching the rule (project a token with `audience: https://api.anthropic.com`, not the IRSA default `sts.amazonaws.com`). ## Scope your rule A `subject_prefix` of `arn:aws:iam::123456789012:role/*` matches every IAM role in the account. Any principal that can assume any matching role can obtain a federated Anthropic token. Lock the rule's `match` block to the narrowest scope that fits your use case: * **Pin the full role ARN:** Use `subject_prefix: "arn:aws:iam:::role/"` with no trailing `*` so other roles in the account do not match. * **Pin the account ID:** Match the `aws_account` field of the token's `https://sts.amazonaws.com/` claim with the `claims` map or a CEL `condition` as a defense-in-depth check against a misconfigured prefix. * **Pin namespace and service account on EKS:** Use the exact `system:serviceaccount::` value with no `*` after the `system:serviceaccount:` prefix. * **Use a separate rule per environment:** Create distinct rules for production, staging, and development workloads rather than widening one prefix to cover them all. ## Next steps * Review the [WIF reference](https://platform.claude.com/docs/en/manage-claude/wif-reference) for the full credential precedence, profile configuration, and rule matching reference. * For self-managed Kubernetes clusters that aren't on EKS, see [Use WIF with Kubernetes](https://platform.claude.com/docs/en/manage-claude/wif-providers/kubernetes). --- title: Use WIF with GitHub Actions url: https://platform.claude.com/docs/en/manage-claude/wif-providers/github-actions description: Authenticate GitHub Actions workflows to the Claude API with short-lived identity tokens instead of long-lived API keys. --- Every GitHub Actions workflow run can request a signed identity token from GitHub's hosted issuer at `https://token.actions.githubusercontent.com`. With Workload Identity Federation, your workflow exchanges that token for a short-lived Anthropic access token, so your CI jobs can call the Claude API without an `ANTHROPIC_API_KEY` secret stored in your repository. The token's `sub` claim encodes the repository and trigger context. For a push to a branch it has the form `repo:/:ref:refs/heads/`. Pull-request runs use `repo:/:pull_request`, and environment-gated deployments use `repo:/:environment:`. Your federation rule matches against this claim (and others, such as `repository_owner` and `ref`) to decide which workflow runs are allowed to authenticate. ## Prerequisites * Familiarity with [WIF concepts](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation#concepts): service accounts, federation issuers, and federation rules. * A GitHub repository where you can edit workflow files and grant the `id-token: write` permission. * Permission to create service accounts, federation issuers, and federation rules in the Claude Console for your Anthropic organization. * Your Anthropic organization ID. You can find it in the Claude Console under **Settings → Organization**. ## Configure your workflow GitHub only issues an identity token to jobs that explicitly request it. Add the `id-token: write` permission at the workflow or job level: ```yaml permissions: id-token: write contents: read ``` Inside the job, the runner exposes two environment variables: `ACTIONS_ID_TOKEN_REQUEST_URL` and `ACTIONS_ID_TOKEN_REQUEST_TOKEN`. Call the request URL with the request token as a bearer credential and your chosen audience as a query parameter, then write the returned JSON Web Token (JWT) to a file: ```yaml - name: Fetch GitHub OIDC token run: | curl -sS -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \ "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://api.anthropic.com" \ | jq -r .value > /tmp/gha-jwt ``` If you prefer JavaScript, `actions/github-script` exposes the same capability through `core.getIDToken(audience)`: ```yaml - name: Fetch GitHub OIDC token uses: actions/github-script@v8 with: script: | const fs = require('fs'); const token = await core.getIDToken('https://api.anthropic.com'); fs.writeFileSync('/tmp/gha-jwt', token); ``` The decoded token carries claims that describe the workflow run. Your federation rule matches against these: ```json { "iss": "https://token.actions.githubusercontent.com", "sub": "repo:your-org/your-repo:ref:refs/heads/main", "aud": "https://api.anthropic.com", "repository": "your-org/your-repo", "repository_owner": "your-org", "ref": "refs/heads/main", "sha": "abc123...", "workflow": "CI", "actor": "octocat", "event_name": "push" } ``` See [GitHub's OIDC subject claim reference](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#example-subject-claims) for the full list of `sub` formats. ## Configure Anthropic In the Claude Console, open **Settings → Workload identity**, click **Connect workload**, and select the **GitHub Actions** tile. The wizard walks you through registering the issuer, creating a service account, and creating a federation rule. The wizard creates these resources for you. Use the following values whether you enter them in the wizard or send them to the [Admin API](https://platform.claude.com/docs/en/manage-claude/wif-admin-api): **Federation issuer:** GitHub publishes its OIDC discovery document and JWKS publicly, so use discovery mode. Anthropic refreshes the keys automatically when GitHub rotates them. ```json { "name": "github-actions", "issuer_url": "https://token.actions.githubusercontent.com", "jwks": { "type": "discovery" } } ``` **Federation rule:** Match only the workflow runs you intend to trust. See [Restrict which workflows can authenticate](https://platform.claude.com/docs/en/manage-claude/wif-providers/github-actions#restrict-which-workflows-can-authenticate) for how to scope these claims safely. ```json { "name": "gha-main", "issuer_id": "fdis_...", "match": { "subject_prefix": "repo:your-org/your-repo:ref:refs/heads/main", "audience": "https://api.anthropic.com", "claims": { "repository_owner": "your-org" } }, "target": { "type": "service_account", "service_account_id": "svac_..." }, "workspace_id": "wrkspc_...", "oauth_scope": "workspace:developer", "token_lifetime_seconds": 600 } ``` Be as specific as the workload allows. Loosen `subject_prefix` to `repo:your-org/your-repo:*` (paired with a `claims.ref` constraint) only if the rule must match multiple event types from the same repository, because the trailing segment of `sub` varies between `ref:...`, `environment:...`, and `pull_request` events. ## Acquire and use a token Set the federation environment variables on the job and call the SDK normally. `Anthropic()` reads `ANTHROPIC_IDENTITY_TOKEN_FILE`, exchanges the JWT on the first request, and refreshes the access token automatically before it expires. ```yaml Workflow name: Call Claude on: push permissions: id-token: write contents: read jobs: call-claude: runs-on: ubuntu-latest env: ANTHROPIC_FEDERATION_RULE_ID: fdrl_... ANTHROPIC_ORGANIZATION_ID: 00000000-0000-0000-0000-000000000000 ANTHROPIC_SERVICE_ACCOUNT_ID: svac_... ANTHROPIC_WORKSPACE_ID: wrkspc_... # required when the rule covers multiple workspaces ANTHROPIC_IDENTITY_TOKEN_FILE: /tmp/gha-jwt steps: - uses: actions/checkout@v5 - name: Fetch GitHub OIDC token run: | curl -sS -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \ "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://api.anthropic.com" \ | jq -r .value > "$ANTHROPIC_IDENTITY_TOKEN_FILE" - name: Run your script run: | pip install anthropic python your_script.py ``` ```bash cURL JWT=$(cat /tmp/gha-jwt) RESPONSE=$(curl -sS https://api.anthropic.com/v1/oauth/token \ -H "content-type: application/json" \ --data @- <messages->create( model: 'claude-opus-5', maxTokens: 1024, messages: [['role' => 'user', 'content' => 'Hello, Claude']], ); $textBlock = array_find($message->content, static fn ($block): bool => $block->type === 'text'); echo $textBlock->text, PHP_EOL; ``` ```ruby Ruby require "anthropic" # Reads ANTHROPIC_FEDERATION_RULE_ID, ANTHROPIC_ORGANIZATION_ID, # ANTHROPIC_SERVICE_ACCOUNT_ID, ANTHROPIC_WORKSPACE_ID, and ANTHROPIC_IDENTITY_TOKEN_FILE # from the job environment. client = Anthropic::Client.new message = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [{role: "user", content: "Hello, Claude"}] ) puts message.content.find { it.type == :text }.text ``` Each GitHub-issued identity token expires roughly five minutes after issuance. The token-request endpoint (`ACTIONS_ID_TOKEN_REQUEST_URL`) stays valid for the entire job, so you can fetch a fresh token at any point. The SDK exchanges the token on first use and caches the resulting Anthropic access token. For jobs that run longer than the Anthropic token's lifetime, the SDK re-reads `ANTHROPIC_IDENTITY_TOKEN_FILE` on each refresh, so re-run the fetch step periodically (or wrap it in a background loop) to keep the file current. Alternatively, pass a token-provider callback to the SDK that calls `ACTIONS_ID_TOKEN_REQUEST_URL` directly instead of using the file path. ## Verify the setup A successful exchange returns an `access_token` beginning with `sk-ant-oat01-` and an `expires_in` value in seconds. On `400 invalid_grant`, see [Troubleshoot a failed exchange](https://platform.claude.com/docs/en/manage-claude/wif-reference#troubleshoot-a-failed-exchange); the most common GitHub Actions-side cause is the `sub` claim format not matching (its trailing segment varies between `ref:...`, `environment:...`, and `pull_request` events). ## Restrict which workflows can authenticate A `subject_prefix` of `repo:your-org/*` alone matches every repository in your organization, and without a `ref` constraint it also matches `pull_request` runs triggered from forks. Anyone who can open a pull request against a matching repository could obtain a federated Anthropic token. Lock the rule's `match` block to the narrowest scope that fits your use case: * **Pin to a single repository:** Use `subject_prefix: "repo:your-org/your-repo:*"` so other repositories in the organization do not match. * **Pin to a protected branch:** Add `"ref": "refs/heads/main"` (or your release branch) under `claims` so pull-request runs and feature branches do not match. * **Pin the owner explicitly:** Add `"repository_owner": "your-org"` under `claims` as a defense-in-depth check against `sub` parsing edge cases. * **Pin to a deployment environment:** For deploy jobs, match `subject_prefix: "repo:your-org/your-repo:environment:production"` and gate that environment with required reviewers in GitHub. ## Next steps * [Workload Identity Federation](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation): full setup walkthrough, environment variables, and credential precedence. * [Authentication](https://platform.claude.com/docs/en/manage-claude/authentication): how federation compares to API keys. --- title: Use WIF with Google Cloud url: https://platform.claude.com/docs/en/manage-claude/wif-providers/gcp description: Federate Google Cloud workloads (Cloud Run, Cloud Functions, App Engine, GCE, GKE) to the Claude API using Google-signed identity tokens instead of static API keys. --- Any Google Cloud compute environment with access to the instance metadata server (Cloud Run, Cloud Functions, App Engine, Compute Engine (GCE), and GKE with Workload Identity) can request a Google-signed identity token for its attached service account. The token's issuer is `https://accounts.google.com`, and Anthropic can validate it directly through standard OIDC discovery, with no extra Google Cloud configuration required. This guide shows how to register the Google issuer with Anthropic, bind a Google service account to an Anthropic service account, and have your workload exchange its identity token for a short-lived Claude API access token. ## Prerequisites * Familiarity with [WIF concepts](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation#concepts): service accounts, federation issuers, and federation rules. * A Google Cloud project with a workload running on Cloud Run, Cloud Functions, App Engine, Compute Engine, or GKE. * A user-managed Google service account attached to that workload (not the Compute Engine default service account). * Permission to create service accounts, federation issuers, and federation rules in the Claude Console for your Anthropic organization. ## Configure Google Cloud Google issues identity tokens automatically to any workload with an attached service account. There is nothing to enable on the Google side beyond attaching the right service account, but the steps differ slightly between standard compute and GKE. Attach a dedicated service account to your service or instance: ```bash CLI gcloud run deploy my-service \ --service-account inference-worker@my-project.iam.gserviceaccount.com ``` Inside the workload, the metadata server returns a signed identity token on demand. Request it with the `audience` you intend to register on the Anthropic side, and include `format=full` so the response carries the `email` claim: ```text wrap GET http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://api.anthropic.com&format=full Metadata-Flavor: Google ``` Or, with the gcloud CLI: ```bash CLI gcloud auth print-identity-token \ --audiences="https://api.anthropic.com" \ --include-email ``` The SDK equivalents are shown in [Acquire and use the token](https://platform.claude.com/docs/en/manage-claude/wif-providers/gcp#acquire-and-use-the-token). The decoded token payload looks like this: ```json { "iss": "https://accounts.google.com", "aud": "https://api.anthropic.com", "sub": "104892...", "azp": "104892...", "email": "inference-worker@my-project.iam.gserviceaccount.com", "email_verified": true, "exp": 1775527120 } ``` The `sub` claim is the Google service account's opaque numeric unique ID. The `email` claim is the human-readable service account address. Match on both `sub` and `email` in your federation rule. Enable [Workload Identity](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity) on your cluster and bind your Kubernetes service account to a Google service account with the `iam.gke.io/gcp-service-account` annotation: ```yaml apiVersion: v1 kind: ServiceAccount metadata: name: inference-worker namespace: prod annotations: iam.gke.io/gcp-service-account: inference-worker@my-project.iam.gserviceaccount.com ``` With this binding in place, the GKE metadata server returns a Google-signed token identical to the Cloud Run and GCE case: same `https://accounts.google.com` issuer, same `email` claim, same fetch URL. Configure Anthropic exactly as in the next section. A `format=full` token from GKE additionally includes `google.compute_engine.project_id`, `google.compute_engine.zone`, and `google.compute_engine.instance_name` claims, which you can reference in a federation rule's `condition` matcher (a CEL expression like `claims.google.compute_engine.project_id == "my-project"`) to scope access to a specific cluster or node pool. If you do not want to bind Kubernetes service accounts to Google service accounts, GKE pods can instead use the cluster's own OIDC issuer (`https://container.googleapis.com/v1/projects/PROJECT/locations/REGION/clusters/CLUSTER`) with a projected `serviceAccountToken` volume. That path uses a per-cluster issuer rather than `accounts.google.com`. See [Use WIF with Kubernetes](https://platform.claude.com/docs/en/manage-claude/wif-providers/kubernetes) for that pattern. ## Configure Anthropic In the Claude Console, open **Settings → Workload identity**, click **Connect workload**, and select the **Google Cloud** tile. The wizard walks you through registering the issuer, creating a service account, and creating a federation rule. The wizard creates these resources for you. Use the following values whether you enter them in the wizard or send them to the [Admin API](https://platform.claude.com/docs/en/manage-claude/wif-admin-api): **Federation issuer:** Google publishes its OIDC discovery document publicly, so use discovery mode. This single issuer covers every Google Cloud surface (Cloud Run, GCE, Cloud Functions, App Engine, and GKE with Workload Identity). Differentiate workloads with rules, not issuers. ```json { "name": "gcp", "issuer_url": "https://accounts.google.com", "jwks": { "type": "discovery" } } ``` **Federation rule:** Match on both the `sub` and `email` claims. `email` is the readable service-account address; `sub` is the service account's numeric unique ID, which Google never reuses, so pinning it protects the rule if the service account is deleted and a new one is later created with the same email. Find the unique ID with `gcloud iam service-accounts describe SA_EMAIL --format='value(uniqueId)'`. ```json { "name": "gcp-inference-worker", "issuer_id": "fdis_...", "match": { "audience": "https://api.anthropic.com", "claims": { "sub": "104892101234567890123", "email": "inference-worker@my-project.iam.gserviceaccount.com" } }, "target": { "type": "service_account", "service_account_id": "svac_..." }, "workspace_id": "wrkspc_...", "oauth_scope": "workspace:developer", "token_lifetime_seconds": 600 } ``` ## Acquire and use the token Inside your Google Cloud workload, fetch the identity token from the metadata server, exchange it at `POST /v1/oauth/token`, and use the returned bearer token to call the Claude API. Each Anthropic SDK handles the exchange and refresh loop for you when you supply a token-provider callable that returns a fresh identity token from the metadata server, as shown in the following examples. ```bash cURL # Fetch the Google-signed identity token from the metadata server JWT=$(curl -sS -H "Metadata-Flavor: Google" \ "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://api.anthropic.com&format=full") # Exchange it for an Anthropic access token RESPONSE=$(curl -sS https://api.anthropic.com/v1/oauth/token \ -H "content-type: application/json" \ --data @- < str: request = google.auth.transport.requests.Request() return google.oauth2.id_token.fetch_id_token(request, AUDIENCE) client = anthropic.Anthropic( credentials=WorkloadIdentityCredentials( identity_token_provider=fetch_google_identity_token, federation_rule_id=os.environ["ANTHROPIC_FEDERATION_RULE_ID"], organization_id=os.environ["ANTHROPIC_ORGANIZATION_ID"], service_account_id=os.environ["ANTHROPIC_SERVICE_ACCOUNT_ID"], workspace_id=os.environ.get("ANTHROPIC_WORKSPACE_ID"), ), ) message = client.messages.create( model="claude-opus-5", max_tokens=1024, messages=[{"role": "user", "content": "Hello from Cloud Run"}], ) print(next(block.text for block in message.content if block.type == "text")) ``` ```typescript TypeScript import Anthropic from "@anthropic-ai/sdk"; import { oidcFederationProvider } from "@anthropic-ai/sdk/lib/credentials/oidc-federation"; const METADATA_URL = "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://api.anthropic.com&format=full"; async function fetchGoogleIdentityToken(): Promise { const response = await fetch(METADATA_URL, { headers: { "Metadata-Flavor": "Google" } }); return response.text(); } const client = new Anthropic({ credentials: oidcFederationProvider({ identityTokenProvider: fetchGoogleIdentityToken, federationRuleId: process.env.ANTHROPIC_FEDERATION_RULE_ID!, organizationId: process.env.ANTHROPIC_ORGANIZATION_ID!, serviceAccountId: process.env.ANTHROPIC_SERVICE_ACCOUNT_ID, workspaceId: process.env.ANTHROPIC_WORKSPACE_ID, baseURL: "https://api.anthropic.com", fetch }) }); const message = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, messages: [{ role: "user", content: "Hello from Cloud Run" }] }); for (const block of message.content) { if (block.type === "text") { console.log(block.text); } } ``` ```go Go const audience = "https://api.anthropic.com" googleIDToken := func(ctx context.Context) (string, error) { creds, err := idtoken.NewCredentials(&idtoken.Options{Audience: audience}) if err != nil { return "", err } tok, err := creds.Token(ctx) if err != nil { return "", err } return tok.Value, nil } client := anthropic.NewClient( option.WithFederationTokenProvider(googleIDToken, option.FederationOptions{ FederationRuleID: os.Getenv("ANTHROPIC_FEDERATION_RULE_ID"), OrganizationID: os.Getenv("ANTHROPIC_ORGANIZATION_ID"), ServiceAccountID: os.Getenv("ANTHROPIC_SERVICE_ACCOUNT_ID"), WorkspaceID: os.Getenv("ANTHROPIC_WORKSPACE_ID"), }), ) message, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Hello from Cloud Run")), }, }) if err != nil { panic(err) } for _, block := range message.Content { if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok { fmt.Println(textBlock.Text) break } } ``` ```java Java HttpClient http = HttpClient.newHttpClient(); HttpRequest metadataRequest = HttpRequest.newBuilder() .uri(URI.create("http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://api.anthropic.com&format=full")) .header("Metadata-Flavor", "Google") .build(); IdentityTokenProvider fetchGoogleIdentityToken = () -> { try { return http.send(metadataRequest, HttpResponse.BodyHandlers.ofString()).body(); } catch (Exception e) { throw new RuntimeException(e); } }; AnthropicClient client = AnthropicOkHttpClient.builder() .federationTokenProvider( fetchGoogleIdentityToken, System.getenv("ANTHROPIC_FEDERATION_RULE_ID"), System.getenv("ANTHROPIC_ORGANIZATION_ID"), System.getenv("ANTHROPIC_SERVICE_ACCOUNT_ID")) .build(); var message = client.messages().create(MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .addUserMessage("Hello from Cloud Run") .build()); IO.println(message.content()); ``` ```csharp C# var credentials = new WorkloadIdentityCredentials(new WorkloadIdentityOptions { FederationRuleId = Environment.GetEnvironmentVariable("ANTHROPIC_FEDERATION_RULE_ID")!, OrganizationId = Environment.GetEnvironmentVariable("ANTHROPIC_ORGANIZATION_ID"), ServiceAccountId = Environment.GetEnvironmentVariable("ANTHROPIC_SERVICE_ACCOUNT_ID"), WorkspaceId = Environment.GetEnvironmentVariable("ANTHROPIC_WORKSPACE_ID"), IdentityTokenProvider = new MetadataTokenProvider(), }); using var client = new AnthropicOidcClient(credentials); var message = await client.Messages.Create(new() { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [new() { Role = Role.User, Content = "Hello from Cloud Run" }], }); foreach (var block in message.Content) { if (block.Value is TextBlock textBlock) { Console.WriteLine(textBlock.Text); } } class MetadataTokenProvider : IIdentityTokenProvider { private const string METADATA_URL = "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://api.anthropic.com&format=full"; private static readonly HttpClient httpClient = new() { DefaultRequestHeaders = { { "Metadata-Flavor", "Google" } }, }; public async Task GetIdentityTokenAsync(CancellationToken ct = default) { return await httpClient.GetStringAsync(METADATA_URL, ct); } } ``` ```bash CLI # Write the Google-signed identity token to a file the CLI can read ANTHROPIC_IDENTITY_TOKEN_FILE=$(mktemp) curl -sS -H "Metadata-Flavor: Google" \ "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://api.anthropic.com&format=full" \ > "$ANTHROPIC_IDENTITY_TOKEN_FILE" export ANTHROPIC_IDENTITY_TOKEN_FILE # ANTHROPIC_FEDERATION_RULE_ID, ANTHROPIC_ORGANIZATION_ID, and # ANTHROPIC_SERVICE_ACCOUNT_ID, and ANTHROPIC_WORKSPACE_ID are read from the environment. ant messages create \ --model claude-opus-5 \ --max-tokens 1024 \ --message '{role: user, content: "Hello from Cloud Run"}' ``` ```php PHP use Anthropic\Client; use Anthropic\Credentials\WorkloadIdentityCredentials; const METADATA_URL = 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://api.anthropic.com&format=full'; $context = stream_context_create([ 'http' => ['header' => "Metadata-Flavor: Google\r\n"], ]); $credentials = new WorkloadIdentityCredentials( identityTokenProvider: fn() => file_get_contents(METADATA_URL, false, $context), federationRuleId: getenv('ANTHROPIC_FEDERATION_RULE_ID'), organizationId: getenv('ANTHROPIC_ORGANIZATION_ID'), serviceAccountId: getenv('ANTHROPIC_SERVICE_ACCOUNT_ID'), workspaceId: getenv('ANTHROPIC_WORKSPACE_ID') ?: null, ); $client = new Client(credentials: $credentials); $message = $client->messages->create( model: 'claude-opus-5', maxTokens: 1024, messages: [['role' => 'user', 'content' => 'Hello from Cloud Run']], ); $textBlock = array_find($message->content, static fn ($block): bool => $block->type === 'text'); echo $textBlock->text, PHP_EOL; ``` ```ruby Ruby require "anthropic" require "net/http" METADATA_URL = "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://api.anthropic.com&format=full" credentials = Anthropic::WorkloadIdentityCredentials.new( identity_token_provider: -> { Net::HTTP.get(URI(METADATA_URL), {"Metadata-Flavor" => "Google"}) }, federation_rule_id: ENV.fetch("ANTHROPIC_FEDERATION_RULE_ID"), organization_id: ENV.fetch("ANTHROPIC_ORGANIZATION_ID"), service_account_id: ENV.fetch("ANTHROPIC_SERVICE_ACCOUNT_ID"), workspace_id: ENV["ANTHROPIC_WORKSPACE_ID"] ) client = Anthropic::Client.new(credentials: credentials) message = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [{role: "user", content: "Hello from Cloud Run"}] ) puts message.content.find { it.type == :text }.text ``` Google identity tokens expire after roughly one hour. The SDKs re-invoke the token provider and re-exchange automatically before expiry. For shell scripts that run longer than the access token's `expires_in`, refresh on a timer and repeat the exchange. ## Verify the setup From inside your workload, decode the identity token and confirm the claims match your rule: ```bash cURL curl -sS -H "Metadata-Flavor: Google" \ "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://api.anthropic.com&format=full" \ | jq -rR 'split(".")[1] | gsub("-";"+") | gsub("_";"/") | @base64d | fromjson' ``` Check that `iss` is `https://accounts.google.com`, `aud` is `https://api.anthropic.com`, and `email` matches the value in your federation rule. Then run the exchange from the previous section. A successful exchange returns an `access_token` beginning with `sk-ant-oat01-` and an `expires_in` value in seconds. On `400 invalid_grant`, see [Troubleshoot a failed exchange](https://platform.claude.com/docs/en/manage-claude/wif-reference#troubleshoot-a-failed-exchange); the most common Google Cloud-side cause is the `email` claim missing (request the token with `format=full` so it is included). ## Scope your rule The Google `sub` claim is the service account's opaque numeric unique ID and has no stable prefix. A `subject_prefix` with a trailing `*` matches arbitrary service accounts across every Google Cloud project, and any of them could obtain a federated Anthropic token. Lock the rule's `match` block to the narrowest scope that fits your use case: * **Match `sub` exactly:** Set the full numeric unique ID in `claims.sub` and never use `subject_prefix` for Google tokens. * **Pin the `email` claim:** Add `claims.email` alongside `sub` so both the stable ID and the readable address must match. * **Pin the audience:** Set `audience` to the exact value you request from the metadata server so tokens minted for other consumers are rejected. * **Pin the project on GKE:** For `format=full` tokens, add a `condition` such as `claims.google.compute_engine.project_id == "my-project"` to restrict the rule to one project's nodes. ## Next steps * Read the [Workload Identity Federation](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation) page for the full resource model and SDK credential precedence. * Add a separate federation rule per environment (production, staging) so you can revoke one without affecting the others. --- title: Use WIF with Kubernetes url: https://platform.claude.com/docs/en/manage-claude/wif-providers/kubernetes description: Authenticate to the Claude API from self-managed Kubernetes clusters using projected service account tokens. --- Self-managed Kubernetes clusters (kubeadm, k3s, OpenShift, and on-premises distributions) sign OIDC JSON Web Tokens (JWTs) for every pod through [projected service account tokens](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#serviceaccount-token-volume-projection). The cluster's API server acts as the OIDC issuer, and each token's `sub` claim follows the form `system:serviceaccount::`. You can find your cluster's issuer URL by reading its discovery document: ```bash cURL kubectl get --raw /.well-known/openid-configuration | jq -r .issuer ``` The mechanism on this page (projected service-account token, cluster API server as the OIDC issuer) is native to Kubernetes itself, so it underlies every Kubernetes distribution. If you run on a managed Kubernetes service, the cloud provider guides walk through where to find the provider-managed issuer URL: [AWS (EKS)](https://platform.claude.com/docs/en/manage-claude/wif-providers/aws#use-eks-projected-service-account-tokens), [Google Cloud (GKE)](https://platform.claude.com/docs/en/manage-claude/wif-providers/gcp), or [Azure (AKS)](https://platform.claude.com/docs/en/manage-claude/wif-providers/azure). If your cluster runs SPIRE, the SPIRE OIDC Discovery Provider is the issuer rather than the cluster API server; see [SPIFFE](https://platform.claude.com/docs/en/manage-claude/wif-providers/spiffe). For any other distribution or a managed provider not listed there, follow this guide and use the issuer URL your cluster reports. ## Prerequisites * Familiarity with [WIF concepts](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation#concepts): service accounts, federation issuers, and federation rules. * A Kubernetes cluster with the [`--service-account-issuer`](https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/) flag configured on the API server. Most distributions set this by default; kubeadm clusters typically use `https://kubernetes.default.svc.cluster.local`. Your platform team can confirm the value if you don't have direct access to the API server configuration. * One of the following so Anthropic can validate token signatures: * The issuer's JWKS endpoint is reachable from the public internet over HTTPS on port 443, or * You can fetch the JWKS from inside the cluster and register it in `inline` mode (covered in [Configure Anthropic](https://platform.claude.com/docs/en/manage-claude/wif-providers/kubernetes#configure-anthropic)). * Permission to create service accounts, federation issuers, and federation rules in the Claude Console for your Anthropic organization. ## Configure Kubernetes Project a service account token into your pod with the audience and lifetime that your federation rule expects. The `serviceAccountToken` projection writes a fresh JWT to the mount path and rotates it before `expirationSeconds` elapses. ```yaml Pod apiVersion: v1 kind: Pod metadata: name: inference-worker namespace: inference spec: serviceAccountName: inference-worker volumes: - name: anthropic-token projected: sources: - serviceAccountToken: audience: https://api.anthropic.com expirationSeconds: 3600 path: token containers: - name: app image: your-registry/inference-worker:latest env: - name: ANTHROPIC_IDENTITY_TOKEN_FILE value: /var/run/secrets/anthropic.com/token - name: ANTHROPIC_FEDERATION_RULE_ID value: fdrl_... - name: ANTHROPIC_ORGANIZATION_ID value: 00000000-0000-0000-0000-000000000000 - name: ANTHROPIC_SERVICE_ACCOUNT_ID value: svac_... - name: ANTHROPIC_WORKSPACE_ID # required when the rule covers multiple workspaces value: wrkspc_... volumeMounts: - name: anthropic-token mountPath: /var/run/secrets/anthropic.com readOnly: true ``` The token issued for this pod carries `sub: "system:serviceaccount:inference:inference-worker"` and `aud: ["https://api.anthropic.com"]`. ## Configure Anthropic In the Claude Console, open **Settings → Workload identity**, click **Connect workload**, and select the **Kubernetes** tile. The wizard walks you through registering the issuer, creating a service account, and creating a federation rule. The wizard creates these resources for you. Use the following values whether you enter them in the wizard or send them to the [Admin API](https://platform.claude.com/docs/en/manage-claude/wif-admin-api): **Federation issuer:** Many self-managed clusters use an issuer URL such as `https://kubernetes.default.svc.cluster.local` that is not reachable from the public internet. If that applies to your cluster, choose the **inline** JWKS source and paste the cluster's keys. Fetch them from inside the cluster: ```bash cURL kubectl get --raw /openid/v1/jwks ``` Then configure the issuer with the contents of the returned `keys` array (not the surrounding `{"keys": [...]}` wrapper): ```json { "name": "onprem-k8s", "issuer_url": "https://kubernetes.default.svc.cluster.local", "jwks": { "type": "inline", "keys": [{ "kty": "RSA", "kid": "...", "n": "...", "e": "AQAB" }] } } ``` In `inline` mode the `issuer_url` is only compared against the JWT's `iss` claim; Anthropic never attempts to reach it. If your issuer is publicly reachable, use `"jwks": {"type": "discovery"}` instead. With `inline` keys you are responsible for updating the issuer when the cluster rotates its service account signing key. Rotation is rare (typically only during cluster upgrades), but token exchanges fail with a signature error until you push the new JWKS. **Federation rule:** Match the service account's `sub` claim and the audience you set on the projected token. ```json { "name": "onprem-inference", "issuer_id": "fdis_...", "match": { "subject_prefix": "system:serviceaccount:inference:inference-worker", "audience": "https://api.anthropic.com" }, "target": { "type": "service_account", "service_account_id": "svac_..." }, "workspace_id": "wrkspc_...", "oauth_scope": "workspace:developer", "token_lifetime_seconds": 600 } ``` Be as specific as the workload allows. Loosen `subject_prefix` to `system:serviceaccount:inference:*` (the trailing `*` makes it a prefix match) only if every service account in the namespace should map to the same Anthropic service account. Add the rule's `fdrl_...` ID to your pod's `ANTHROPIC_FEDERATION_RULE_ID` environment variable. ## Acquire and use the token The pod spec in [Configure Kubernetes](https://platform.claude.com/docs/en/manage-claude/wif-providers/kubernetes#configure-kubernetes) sets `ANTHROPIC_IDENTITY_TOKEN_FILE` to the projected mount path, along with `ANTHROPIC_FEDERATION_RULE_ID`, `ANTHROPIC_ORGANIZATION_ID`, `ANTHROPIC_SERVICE_ACCOUNT_ID`, and `ANTHROPIC_WORKSPACE_ID`. With those in place, the SDK reads the token from disk on every exchange and refreshes the Anthropic access token automatically. ```bash cURL JWT=$(cat "$ANTHROPIC_IDENTITY_TOKEN_FILE") ACCESS_TOKEN=$(curl -sS https://api.anthropic.com/v1/oauth/token \ -H "content-type: application/json" \ --data @- <messages->create( model: 'claude-opus-5', maxTokens: 1024, messages: [['role' => 'user', 'content' => 'Hello, Claude']], ); $textBlock = array_find($message->content, static fn ($block): bool => $block->type === 'text'); echo $textBlock->text, PHP_EOL; ``` ```ruby Ruby require "anthropic" # Reads ANTHROPIC_FEDERATION_RULE_ID, ANTHROPIC_ORGANIZATION_ID, # ANTHROPIC_SERVICE_ACCOUNT_ID, ANTHROPIC_WORKSPACE_ID, and ANTHROPIC_IDENTITY_TOKEN_FILE client = Anthropic::Client.new message = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [{role: "user", content: "Hello, Claude"}] ) puts message.content.find { it.type == :text }.text ``` ## Verify the setup A successful exchange returns an `access_token` beginning with `sk-ant-oat01-` and an `expires_in` value in seconds. On `400 invalid_grant`, see [Troubleshoot a failed exchange](https://platform.claude.com/docs/en/manage-claude/wif-reference#troubleshoot-a-failed-exchange); the most common Kubernetes-side cause is a JWKS key mismatch (for `inline` mode, re-fetch with `kubectl get --raw /openid/v1/jwks` and update the issuer). ## Scope your rule A `subject_prefix` of `system:serviceaccount:*` matches every service account in the cluster, so any pod can obtain a federated Anthropic token. Without an `audience` matcher, the rule also matches the cluster's default-audience tokens, which every pod already has projected. Lock the rule's `match` block to the narrowest scope that fits your use case: * **Pin namespace and service-account name:** Use the full `system:serviceaccount::` value with no trailing `*`. * **Always set an audience:** Require `audience` on the rule and set the same value on the pod's `serviceAccountToken` projection so default-audience tokens are rejected. * **Use a separate rule per namespace:** Create a distinct rule and Anthropic service account for each namespace rather than widening one rule. * **Scope inline-JWKS issuers to one cluster:** When several clusters share an issuer URL, register each cluster's JWKS as its own federation issuer and bind rules to that issuer only. ## Next steps * [Workload Identity Federation](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation): concepts, the token-exchange flow, and SDK configuration options. * [WIF reference](https://platform.claude.com/docs/en/manage-claude/wif-reference): environment variables, JWKS source modes, and rule match modes. --- title: Use WIF with Microsoft Entra ID url: https://platform.claude.com/docs/en/manage-claude/wif-providers/azure description: Federate Azure managed identities and Entra Workload Identity with the Claude API so your Azure workloads can call Claude without static API keys. --- Azure workloads authenticate to the Claude API by presenting a JSON Web Token (JWT) issued by Microsoft Entra ID, then exchanging it for a short-lived Anthropic access token. The setup follows the same shape on every Azure platform: 1. **Register the token audience:** Create one app registration in your Microsoft Entra tenant to represent the Claude API audience. Every workload in the tenant requests Entra tokens for it. 2. **Set up the identity for your platform:** A managed identity on VMs, VM Scale Sets, App Service, Functions, and Container Apps, or Entra Workload Identity on AKS. 3. **Configure Anthropic:** Register your tenant's Entra issuer, create a service account, and write a federation rule that matches the token's claims. 4. **Exchange at runtime:** Your workload exchanges its Entra-issued token at `POST /v1/oauth/token` for an `sk-ant-oat01-...` Anthropic access token and calls Claude with it. On both paths the token you present to Anthropic carries your tenant-specific Entra issuer and the managed identity's object ID in the `sub` and `oid` claims; only how the workload obtains that token differs. Pick the section for where your workload runs: [Use a managed identity](https://platform.claude.com/docs/en/manage-claude/wif-providers/azure#use-a-managed-identity) for VMs, VM Scale Sets, App Service, Functions, or Container Apps; [Use Entra Workload Identity on AKS](https://platform.claude.com/docs/en/manage-claude/wif-providers/azure#use-entra-workload-identity-on-aks) for AKS. ## Prerequisites * Familiarity with [WIF concepts](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation#concepts): service accounts, federation issuers, and federation rules. * An Azure subscription with permission to assign managed identities (or configure Entra Workload Identity on AKS). * Permission to create one app registration and service principal in your Microsoft Entra tenant (the shared Claude API audience). Entra only issues tokens for an audience that exists in the tenant, so the [Register the token audience](https://platform.claude.com/docs/en/manage-claude/wif-providers/azure#register-the-token-audience) step is required before any token request succeeds. * Your Microsoft Entra tenant ID. Find it in the Azure portal under **Microsoft Entra ID → Overview → Tenant ID**. * Permission to create service accounts, federation issuers, and federation rules in the Claude Console for your Anthropic organization. ## Register the token audience Microsoft Entra ID only issues a token when the requested audience exists in your tenant as an app registration with a service principal. Create one app registration to represent the Claude API audience; every workload in the tenant can request tokens for it. Without this registration, token requests fail with a "resource not found in tenant" error (`AADSTS50001` from the managed identity endpoints, `AADSTS500011` from the Entra token endpoint). ```bash # Create the app registration that represents the Claude API audience. APP_ID=$(az ad app create --display-name claude-api-federation --query appId -o tsv) # Request v2.0 access tokens and set the api:// identifier URI. az ad app update --id "$APP_ID" \ --identifier-uris "api://$APP_ID" \ --set api.requestedAccessTokenVersion=2 # Create the service principal so the audience resolves in your tenant. az ad sp create --id "$APP_ID" ``` Use the `api://` identifier URI format. Entra restricts `https://` identifier URIs to verified domains of your own tenant, so a URI such as `https://api.anthropic.com` cannot be registered in most tenants; `api://` is accepted everywhere. With `requestedAccessTokenVersion: 2`, tokens for this audience are v2.0, which is what this guide assumes. If you reuse an existing registration that emits v1.0 tokens, see [If your tokens are v1.0](https://platform.claude.com/docs/en/manage-claude/wif-providers/azure#if-your-tokens-are-v1-0). ## Use a managed identity Use this path when your workload runs on a VM, a VM Scale Set, App Service, Functions, or Container Apps. The workload requests an Entra-issued JWT for its assigned managed identity from the platform's local token endpoint, then exchanges that JWT with Anthropic. ### Configure the managed identity Enable a system-assigned or user-assigned managed identity on your Azure resource. In the Azure portal, open the resource, go to **Identity**, and turn on **System assigned** (or attach a user-assigned identity). After the identity is created, note its **Object (principal) ID**. This GUID appears as both the `sub` and `oid` claims in the issued token, and your Anthropic federation rule will match on it. You can find it on the resource's **Identity** page; for a user-assigned identity, it is the **Object (principal) ID** on the managed identity resource's **Overview** page. (A managed identity has only a service principal in Microsoft Entra ID, not an app registration.) The platform exposes a local token endpoint once the identity is attached: * **VMs and VM Scale Sets:** IMDS at `http://169.254.169.254/metadata/identity/oauth2/token` with the header `Metadata: true` and `api-version=2018-02-01`. * **App Service, Functions, and Container Apps:** The URL in the `IDENTITY_ENDPOINT` environment variable with the header `X-IDENTITY-HEADER` set to the value of `IDENTITY_HEADER`, and `api-version=2019-08-01`. IMDS is not reachable on these platforms. If the resource has more than one user-assigned managed identity, add `client_id=` to the token request to select one. Azure recommends always specifying it. Without it, the outcome depends on whether the resource also has a system-assigned identity enabled: if it does, the request silently falls back to that identity and then fails your federation rule's `oid` match; if it does not, the request fails outright as soon as a second user-assigned identity is attached. Request a token from the endpoint and decode its payload to confirm the claims your federation rule needs to match. (For the decode command, see [Troubleshoot a failed exchange](https://platform.claude.com/docs/en/manage-claude/wif-reference#troubleshoot-a-failed-exchange).) A v2.0 token for a managed identity carries these claims: ```json { "iss": "https://login.microsoftonline.com//v2.0", "sub": "9f8e7d6c-1a2b-3c4d-5e6f-...", "aud": "", "oid": "9f8e7d6c-1a2b-3c4d-5e6f-...", "tid": "", "azp": "", "ver": "2.0", "exp": 1775527120 } ``` | Claim | Value | Match this when | | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `oid` | The managed identity's object ID, identical to `sub` | You want to authorize one specific managed identity. This is the default; the rule in [Configure Anthropic](https://platform.claude.com/docs/en/manage-claude/wif-providers/azure#configure-anthropic) matches it. | | `azp` | The calling identity's client ID | You want to authorize every workload that shares one app registration. For a managed identity, `azp` is unique to that identity, so it is equivalent to `oid`. | | `aud` | The audience app registration's client ID (the `` GUID from [Register the token audience](https://platform.claude.com/docs/en/manage-claude/wif-providers/azure#register-the-token-audience)) | Always. The rule's `audience` field must equal the token's `aud` value exactly. | | `tid` | Your tenant ID | You want defense in depth. The issuer URL already pins the tenant. | If the decoded token's `ver` claim is `1.0`, the claim names and values differ. See [If your tokens are v1.0](https://platform.claude.com/docs/en/manage-claude/wif-providers/azure#if-your-tokens-are-v1-0) before continuing. ### Configure Anthropic In the Claude Console, open **Settings → Workload identity**, click **Connect workload**, and select the **Microsoft Entra** tile. The wizard walks you through registering the issuer, creating a service account, and creating a federation rule. The wizard creates these resources for you. Use the following values whether you enter them in the wizard or send them to the [Admin API](https://platform.claude.com/docs/en/manage-claude/wif-admin-api): **Federation issuer:** Choose **v2.0 (login.microsoftonline.com)** in the wizard's **Token issuer** selector. (The selector defaults to v1; that default exists for tenants reusing older registrations that still emit v1.0 tokens.) Entra publishes an OIDC discovery document at the per-tenant issuer URL, so use discovery mode. Each Microsoft Entra tenant you federate needs its own issuer record. ```json { "name": "azure-prod-tenant", "issuer_url": "https://login.microsoftonline.com//v2.0", "jwks": { "type": "discovery" }, "max_jwt_lifetime_seconds": 86400 } ``` Managed identity workloads need `max_jwt_lifetime_seconds: 86400`. Azure issues managed identity tokens with up to 24 hours between `iat` and `exp` because it caches each resource's token for that window and offers no way to force an early refresh, and the issuer's 1-hour default rejects those tokens with `invalid_grant`. The Connect workload wizard's Microsoft Entra tile creates the issuer with `max_jwt_lifetime_seconds` set to `7500` and provides no field to change it during creation, so finish the wizard, then open **Settings → Workload identity → Issuers**, edit the issuer, and raise the value to `86400`. You can also update the issuer through the Admin API. A longer accepted lifetime means a leaked Entra token stays exchangeable for longer. If a token leaks, the lever is disabling the federation rule; a tight `oid` match limits which identities can exchange a token in the first place, as described in [Scope your rule](https://platform.claude.com/docs/en/manage-claude/wif-providers/azure#scope-your-rule). **Federation rule:** Match on the managed identity's object ID and your tenant ID. For the v2.0 tokens this guide configures, the `audience` value is the audience app registration's client ID (the `` GUID from [Register the token audience](https://platform.claude.com/docs/en/manage-claude/wif-providers/azure#register-the-token-audience)). Use the exact `aud` value from your decoded token. ```json { "name": "azure-inference-worker", "issuer_id": "fdis_...", "match": { "audience": "", "claims": { "oid": "9f8e7d6c-1a2b-3c4d-5e6f-...", "tid": "" } }, "target": { "type": "service_account", "service_account_id": "svac_..." }, "workspace_id": "wrkspc_...", "oauth_scope": "workspace:developer", "token_lifetime_seconds": 600 } ``` `token_lifetime_seconds` is the lifetime of the Anthropic access token the exchange returns, not of the Entra token; the SDK refreshes it for you. ### Acquire and use the token At runtime your workload fetches its Entra token, exchanges it at `POST /v1/oauth/token`, and uses the returned bearer token to call Claude. Each Anthropic SDK handles the exchange and refresh loop when you supply a token-provider callable, as shown in the following examples. The cURL tab shows the raw flow. The samples fetch the managed identity token from the platform's token endpoint: IMDS on VMs and VM Scale Sets, or the `IDENTITY_ENDPOINT` service on App Service, Functions, and Container Apps. Replace `` in the `api://` resource value with the audience app registration's client ID from [Register the token audience](https://platform.claude.com/docs/en/manage-claude/wif-providers/azure#register-the-token-audience). If your workload already uses the Azure Identity client library, pass its token acquisition (`DefaultAzureCredential` with the scope `api:///.default`) as the identity token provider instead of calling the token endpoints directly. The library selects the correct endpoint on every Azure platform, including AKS with Entra Workload Identity. ```bash cURL # 1. Fetch the Entra-issued token (managed identity). # On a VM or VM Scale Set, use IMDS. With multiple user-assigned # identities, append &client_id=. ENTRA_TOKEN=$(curl -sS -H "Metadata: true" \ "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=api://" \ | jq -r .access_token) # On App Service, Functions, or Container Apps, use the local token # service instead (IMDS is not reachable there): # ENTRA_TOKEN=$(curl -sS -H "X-IDENTITY-HEADER: $IDENTITY_HEADER" \ # "$IDENTITY_ENDPOINT?api-version=2019-08-01&resource=api://" \ # | jq -r .access_token) # For AKS with Entra Workload Identity, use the two-hop exchange in the # "Use Entra Workload Identity on AKS" section instead. # 2. Exchange it for an Anthropic access token. RESPONSE=$(curl -sS https://api.anthropic.com/v1/oauth/token \ -H "content-type: application/json" \ -d @- < str: """Fetch a managed identity token from the platform's token endpoint.""" # With multiple user-assigned identities, add client_id= # to the request params to select one. if endpoint := os.environ.get("IDENTITY_ENDPOINT"): # App Service, Functions, Container Apps response = requests.get( endpoint, headers={"X-IDENTITY-HEADER": os.environ["IDENTITY_HEADER"]}, params={"api-version": "2019-08-01", "resource": AUDIENCE}, timeout=5, ) else: # VM or VM Scale Set: Azure Instance Metadata Service (IMDS) response = requests.get( "http://169.254.169.254/metadata/identity/oauth2/token", headers={"Metadata": "true"}, params={"api-version": "2018-02-01", "resource": AUDIENCE}, timeout=5, ) response.raise_for_status() return response.json()["access_token"] client = anthropic.Anthropic( credentials=WorkloadIdentityCredentials( identity_token_provider=fetch_entra_token, federation_rule_id=os.environ["ANTHROPIC_FEDERATION_RULE_ID"], organization_id=os.environ["ANTHROPIC_ORGANIZATION_ID"], service_account_id=os.environ["ANTHROPIC_SERVICE_ACCOUNT_ID"], workspace_id=os.environ.get("ANTHROPIC_WORKSPACE_ID"), ), ) message = client.messages.create( model="claude-opus-5", max_tokens=1024, messages=[{"role": "user", "content": "Hello from Azure"}], ) print(next(block.text for block in message.content if block.type == "text")) ``` ```typescript TypeScript import Anthropic from "@anthropic-ai/sdk"; import { oidcFederationProvider } from "@anthropic-ai/sdk/lib/credentials/oidc-federation"; // The audience app registration's identifier URI (see Register the token audience). const AUDIENCE = "api://"; async function fetchEntraToken(): Promise { // App Service, Functions, and Container Apps inject IDENTITY_ENDPOINT; // VMs and VM Scale Sets use IMDS. // With multiple user-assigned identities, append &client_id=. const identityEndpoint = process.env.IDENTITY_ENDPOINT; const url = identityEndpoint ? `${identityEndpoint}?api-version=2019-08-01&resource=${AUDIENCE}` : `http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=${AUDIENCE}`; const headers: Record = identityEndpoint ? { "X-IDENTITY-HEADER": process.env.IDENTITY_HEADER! } : { Metadata: "true" }; const response = await fetch(url, { headers }); const body = (await response.json()) as { access_token: string }; return body.access_token; } const client = new Anthropic({ credentials: oidcFederationProvider({ identityTokenProvider: fetchEntraToken, federationRuleId: process.env.ANTHROPIC_FEDERATION_RULE_ID!, organizationId: process.env.ANTHROPIC_ORGANIZATION_ID!, serviceAccountId: process.env.ANTHROPIC_SERVICE_ACCOUNT_ID, workspaceId: process.env.ANTHROPIC_WORKSPACE_ID, baseURL: "https://api.anthropic.com", fetch }) }); const message = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, messages: [{ role: "user", content: "Hello from Azure" }] }); for (const block of message.content) { if (block.type === "text") { console.log(block.text); } } ``` ```go Go package main import ( "context" "encoding/json" "fmt" "net/http" "os" "github.com/anthropics/anthropic-sdk-go" "github.com/anthropics/anthropic-sdk-go/option" ) // The audience app registration's identifier URI (see Register the token audience). const audience = "api://" // fetchEntraToken fetches a managed identity token from the platform's token // endpoint: IMDS on VMs and VM Scale Sets, or the IDENTITY_ENDPOINT service // on App Service, Functions, and Container Apps. func fetchEntraToken(ctx context.Context) (string, error) { // With multiple user-assigned identities, append &client_id=. tokenURL := "http://169.254.169.254/metadata/identity/oauth2/token" + "?api-version=2018-02-01&resource=" + audience header, value := "Metadata", "true" if endpoint := os.Getenv("IDENTITY_ENDPOINT"); endpoint != "" { tokenURL = endpoint + "?api-version=2019-08-01&resource=" + audience header, value = "X-IDENTITY-HEADER", os.Getenv("IDENTITY_HEADER") } req, err := http.NewRequestWithContext(ctx, http.MethodGet, tokenURL, nil) if err != nil { return "", err } req.Header.Set(header, value) resp, err := http.DefaultClient.Do(req) if err != nil { return "", fmt.Errorf("call token endpoint: %w", err) } defer resp.Body.Close() var body struct { AccessToken string `json:"access_token"` } if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { return "", fmt.Errorf("decode token response: %w", err) } return body.AccessToken, nil } func main() { client := anthropic.NewClient( option.WithFederationTokenProvider(fetchEntraToken, option.FederationOptions{ FederationRuleID: os.Getenv("ANTHROPIC_FEDERATION_RULE_ID"), OrganizationID: os.Getenv("ANTHROPIC_ORGANIZATION_ID"), ServiceAccountID: os.Getenv("ANTHROPIC_SERVICE_ACCOUNT_ID"), WorkspaceID: os.Getenv("ANTHROPIC_WORKSPACE_ID"), }), ) message, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Hello from Azure")), }, }) if err != nil { panic(err) } for _, block := range message.Content { if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok { fmt.Println(textBlock.Text) break } } } ``` ```java Java HttpClient http = HttpClient.newHttpClient(); // The audience app registration's identifier URI (see Register the token audience). String audience = "api://"; // App Service, Functions, and Container Apps inject IDENTITY_ENDPOINT; // VMs and VM Scale Sets use IMDS. // With multiple user-assigned identities, append &client_id=. String identityEndpoint = System.getenv("IDENTITY_ENDPOINT"); HttpRequest tokenRequest = identityEndpoint != null ? HttpRequest.newBuilder(URI.create(identityEndpoint + "?api-version=2019-08-01&resource=" + audience)) .header("X-IDENTITY-HEADER", System.getenv("IDENTITY_HEADER")) .build() : HttpRequest.newBuilder(URI.create("http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=" + audience)) .header("Metadata", "true") .build(); IdentityTokenProvider fetchEntraToken = () -> { try { var response = http.send(tokenRequest, HttpResponse.BodyHandlers.ofString()); return new ObjectMapper().readTree(response.body()).get("access_token").asText(); } catch (Exception e) { throw new RuntimeException(e); } }; AnthropicClient client = AnthropicOkHttpClient.builder() .federationTokenProvider( fetchEntraToken, System.getenv("ANTHROPIC_FEDERATION_RULE_ID"), System.getenv("ANTHROPIC_ORGANIZATION_ID"), System.getenv("ANTHROPIC_SERVICE_ACCOUNT_ID"), System.getenv("ANTHROPIC_WORKSPACE_ID")) .build(); var message = client.messages().create(MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .addUserMessage("Hello from Azure") .build()); IO.println(message.content()); ``` ```csharp C# var credentials = new WorkloadIdentityCredentials(new WorkloadIdentityOptions { FederationRuleId = Environment.GetEnvironmentVariable("ANTHROPIC_FEDERATION_RULE_ID")!, OrganizationId = Environment.GetEnvironmentVariable("ANTHROPIC_ORGANIZATION_ID"), ServiceAccountId = Environment.GetEnvironmentVariable("ANTHROPIC_SERVICE_ACCOUNT_ID"), WorkspaceId = Environment.GetEnvironmentVariable("ANTHROPIC_WORKSPACE_ID"), IdentityTokenProvider = new EntraTokenProvider(), }); using var client = new AnthropicOidcClient(credentials); var message = await client.Messages.Create(new() { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [new() { Role = Role.User, Content = "Hello from Azure" }], }); foreach (var block in message.Content) { if (block.Value is TextBlock textBlock) { Console.WriteLine(textBlock.Text); } } class EntraTokenProvider : IIdentityTokenProvider { // The audience app registration's identifier URI (see Register the token audience). private const string Audience = "api://"; private static readonly HttpClient httpClient = new(); public async Task GetIdentityTokenAsync(CancellationToken ct = default) { // App Service, Functions, and Container Apps inject IDENTITY_ENDPOINT; // VMs and VM Scale Sets use IMDS. // With multiple user-assigned identities, append &client_id=. var identityEndpoint = Environment.GetEnvironmentVariable("IDENTITY_ENDPOINT"); using var request = identityEndpoint is not null ? new HttpRequestMessage(HttpMethod.Get, $"{identityEndpoint}?api-version=2019-08-01&resource={Audience}") { Headers = { { "X-IDENTITY-HEADER", Environment.GetEnvironmentVariable("IDENTITY_HEADER") } }, } : new HttpRequestMessage(HttpMethod.Get, $"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource={Audience}") { Headers = { { "Metadata", "true" } }, }; using var response = await httpClient.SendAsync(request, ct); response.EnsureSuccessStatusCode(); using var json = await JsonDocument.ParseAsync( await response.Content.ReadAsStreamAsync(ct), default, ct); return json.RootElement.GetProperty("access_token").GetString()!; } } ``` ```php PHP use Anthropic\Client; use Anthropic\Credentials\WorkloadIdentityCredentials; // The audience app registration's identifier URI (see Register the token audience). const AUDIENCE = 'api://'; function fetchEntraToken(): string { // App Service, Functions, and Container Apps inject IDENTITY_ENDPOINT; // VMs and VM Scale Sets use IMDS. // With multiple user-assigned identities, append &client_id=. $identityEndpoint = getenv('IDENTITY_ENDPOINT'); if ($identityEndpoint !== false) { $url = $identityEndpoint . '?api-version=2019-08-01&resource=' . AUDIENCE; $header = 'X-IDENTITY-HEADER: ' . getenv('IDENTITY_HEADER'); } else { $url = 'http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=' . AUDIENCE; $header = 'Metadata: true'; } $context = stream_context_create([ 'http' => ['header' => $header . "\r\n"], ]); $body = json_decode(file_get_contents($url, false, $context), true); return $body['access_token']; } $credentials = new WorkloadIdentityCredentials( identityTokenProvider: fetchEntraToken(...), federationRuleId: getenv('ANTHROPIC_FEDERATION_RULE_ID'), organizationId: getenv('ANTHROPIC_ORGANIZATION_ID'), serviceAccountId: getenv('ANTHROPIC_SERVICE_ACCOUNT_ID'), workspaceId: getenv('ANTHROPIC_WORKSPACE_ID') ?: null, ); $client = new Client(credentials: $credentials); $message = $client->messages->create( model: 'claude-opus-5', maxTokens: 1024, messages: [['role' => 'user', 'content' => 'Hello from Azure']], ); $textBlock = array_find($message->content, static fn ($block): bool => $block->type === 'text'); echo $textBlock->text, PHP_EOL; ``` ```ruby Ruby require "anthropic" require "json" require "net/http" # The audience app registration's identifier URI (see Register the token audience). AUDIENCE = "api://" def fetch_entra_token # App Service, Functions, and Container Apps inject IDENTITY_ENDPOINT; # VMs and VM Scale Sets use IMDS. # With multiple user-assigned identities, append &client_id=. if (endpoint = ENV["IDENTITY_ENDPOINT"]) url = "#{endpoint}?api-version=2019-08-01&resource=#{AUDIENCE}" headers = {"X-IDENTITY-HEADER" => ENV.fetch("IDENTITY_HEADER")} else url = "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=#{AUDIENCE}" headers = {"Metadata" => "true"} end response = Net::HTTP.get(URI(url), headers) JSON.parse(response).fetch("access_token") end credentials = Anthropic::WorkloadIdentityCredentials.new( identity_token_provider: -> { fetch_entra_token }, federation_rule_id: ENV.fetch("ANTHROPIC_FEDERATION_RULE_ID"), organization_id: ENV.fetch("ANTHROPIC_ORGANIZATION_ID"), service_account_id: ENV.fetch("ANTHROPIC_SERVICE_ACCOUNT_ID"), workspace_id: ENV["ANTHROPIC_WORKSPACE_ID"] ) client = Anthropic::Client.new(credentials: credentials) message = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [{role: "user", content: "Hello from Azure"}] ) puts message.content.find { it.type == :text }.text ``` ```bash CLI # Write the Entra-issued access token to a file the CLI can read. # Shown for a VM or VM Scale Set (IMDS). On App Service, Functions, or # Container Apps, fetch from "$IDENTITY_ENDPOINT?api-version=2019-08-01&resource=api://" # with -H "X-IDENTITY-HEADER: $IDENTITY_HEADER" instead. # With multiple user-assigned identities, append &client_id=. ANTHROPIC_IDENTITY_TOKEN_FILE=$(mktemp) trap 'rm -f "$ANTHROPIC_IDENTITY_TOKEN_FILE"' EXIT curl -sS -H "Metadata: true" \ "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=api://" \ | jq -r .access_token > "$ANTHROPIC_IDENTITY_TOKEN_FILE" export ANTHROPIC_IDENTITY_TOKEN_FILE # ANTHROPIC_FEDERATION_RULE_ID, ANTHROPIC_ORGANIZATION_ID, # ANTHROPIC_SERVICE_ACCOUNT_ID, and ANTHROPIC_WORKSPACE_ID are read from the environment. ant messages create \ --model claude-opus-5 \ --max-tokens 1024 \ --message '{role: user, content: "Hello from Azure"}' ``` ### Verify the setup From your Azure resource, run the cURL exchange shown in [Acquire and use the token](https://platform.claude.com/docs/en/manage-claude/wif-providers/azure#acquire-and-use-the-token) and confirm that `POST /v1/oauth/token` returns a `200` with an `access_token` beginning with `sk-ant-oat01-` and an `expires_in` value in seconds. On `400 invalid_grant`, decode the Entra token (see [Troubleshoot a failed exchange](https://platform.claude.com/docs/en/manage-claude/wif-reference#troubleshoot-a-failed-exchange) for the command) and check the most common Azure-side causes: * **Issuer mismatch:** The registered `issuer_url` must match the token's `iss` claim exactly. A v2.0 token carries `https://login.microsoftonline.com//v2.0`; if the decoded `ver` claim is `1.0`, see [If your tokens are v1.0](https://platform.claude.com/docs/en/manage-claude/wif-providers/azure#if-your-tokens-are-v1-0). * **Token lifetime:** Managed identity tokens carry up to 24 hours between `iat` and `exp`. If the issuer still has the wizard's `7500` (or the 1-hour default), raise `max_jwt_lifetime_seconds` to `86400` as described in [Configure Anthropic](https://platform.claude.com/docs/en/manage-claude/wif-providers/azure#configure-anthropic). * **Audience mismatch:** The rule's `audience` must equal the token's `aud` exactly: the audience app registration's client ID for the v2.0 tokens this guide configures. * **Claim name mismatch:** A rule that matches on a claim the token does not carry never passes. v1.0 tokens carry the client ID in `appid`, not `azp`; see [If your tokens are v1.0](https://platform.claude.com/docs/en/manage-claude/wif-providers/azure#if-your-tokens-are-v1-0). ## Use Entra Workload Identity on AKS Use this path when your workload runs in an AKS pod. Entra Workload Identity federates a Kubernetes service account with a user-assigned managed identity: Kubernetes projects a service account token (signed by the AKS cluster's OIDC issuer) into the pod at the path in `AZURE_FEDERATED_TOKEN_FILE`. That projected token is not an Entra-issued token, so to stay on the Entra-mediated path described on this page, the workload performs a two-hop exchange: it first redeems the projected token at `https://login.microsoftonline.com//oauth2/v2.0/token` (federated `client_credentials` grant) for an Entra-issued access token, then passes that Entra token to the Anthropic SDK as the identity token. AKS pods can alternatively skip the Entra exchange and present the Kubernetes-projected service account token to Anthropic directly. That path registers your AKS cluster's OIDC issuer with Anthropic instead of your Entra tenant. See [Use WIF with Kubernetes](https://platform.claude.com/docs/en/manage-claude/wif-providers/kubernetes) for that flow. ### Configure Entra Workload Identity Enabling workload identity installs the `azure-workload-identity` mutating webhook for you; deploy it manually only on non-AKS clusters. Capture the cluster's OIDC issuer URL for the federated credential you create in a later step. ```bash az aks update \ --resource-group \ --name \ --enable-oidc-issuer \ --enable-workload-identity AKS_OIDC_ISSUER=$(az aks show \ --resource-group \ --name \ --query oidcIssuerProfile.issuerUrl -o tsv) ``` Capture two values from the identity: the **Client ID** goes into the service account annotation (and is injected into the pod as `AZURE_CLIENT_ID`), and the **Object (principal) ID** appears as the `oid` claim that your Anthropic federation rule matches. ```bash az identity create \ --resource-group \ --name claude-inference-identity \ --location # Goes in the service account annotation; injected into the pod as AZURE_CLIENT_ID. IDENTITY_CLIENT_ID=$(az identity show \ --resource-group \ --name claude-inference-identity \ --query clientId -o tsv) # Appears as the oid claim that your federation rule matches. IDENTITY_OBJECT_ID=$(az identity show \ --resource-group \ --name claude-inference-identity \ --query principalId -o tsv) ``` The `azure-workload-identity` webhook reads the `azure.workload.identity/client-id` annotation to inject `AZURE_CLIENT_ID` into the pod, which the samples in [Acquire and use the token](https://platform.claude.com/docs/en/manage-claude/wif-providers/azure#acquire-and-use-the-token-2) read from the environment. ```yaml apiVersion: v1 kind: ServiceAccount metadata: name: claude-inference namespace: inference annotations: azure.workload.identity/client-id: ``` The federated credential trusts your cluster's OIDC issuer for that specific service account. The `--audience api://AzureADTokenExchange` value is Entra's fixed audience for incoming Kubernetes service account tokens; it is unrelated to the Claude API audience you registered earlier. ```bash az identity federated-credential create \ --resource-group \ --identity-name claude-inference-identity \ --name claude-inference-aks \ --issuer "$AKS_OIDC_ISSUER" \ --subject system:serviceaccount:inference:claude-inference \ --audience api://AzureADTokenExchange ``` The pod must carry the `azure.workload.identity/use: "true"` label and run as the annotated service account. The webhook then injects `AZURE_FEDERATED_TOKEN_FILE`, `AZURE_CLIENT_ID`, and `AZURE_TENANT_ID` into the pod. The file at `AZURE_FEDERATED_TOKEN_FILE` contains the Kubernetes-projected service account token, signed by the AKS cluster's OIDC issuer. ```yaml apiVersion: v1 kind: Pod metadata: name: inference-worker namespace: inference labels: azure.workload.identity/use: "true" spec: serviceAccountName: claude-inference containers: - name: app image: your-registry/inference-worker:latest ``` The token your Anthropic federation rule sees is not the projected file; it is the Entra-issued token returned by the `client_credentials` exchange. From inside a labeled pod, run step 1 of the cURL sample in [Acquire and use the token](https://platform.claude.com/docs/en/manage-claude/wif-providers/azure#acquire-and-use-the-token-2) and decode the result. It carries the same claim shape as the managed identity path: ```json { "iss": "https://login.microsoftonline.com//v2.0", "sub": "9f8e7d6c-1a2b-3c4d-5e6f-...", "aud": "", "oid": "9f8e7d6c-1a2b-3c4d-5e6f-...", "tid": "", "azp": "", "ver": "2.0", "exp": 1775527120 } ``` `sub` and `oid` are the managed identity's object ID, `aud` is the audience app registration's client ID, and `azp` is the managed identity's client ID (the value of `AZURE_CLIENT_ID`). The lifetime differs from the managed identity path: `client_credentials` tokens default to a random 60 to 90 minute window between `iat` and `exp`, not 24 hours. ### Configure Anthropic In the Claude Console, open **Settings → Workload identity**, click **Connect workload**, and select the **Microsoft Entra** tile. The wizard walks you through registering the issuer, creating a service account, and creating a federation rule. The wizard creates these resources for you. Use the following values whether you enter them in the wizard or send them to the [Admin API](https://platform.claude.com/docs/en/manage-claude/wif-admin-api): **Federation issuer:** Choose **v2.0 (login.microsoftonline.com)** in the wizard's **Token issuer** selector. (The selector defaults to v1; that default exists for tenants reusing older registrations that still emit v1.0 tokens.) Entra publishes an OIDC discovery document at the per-tenant issuer URL, so use discovery mode. Each Microsoft Entra tenant you federate needs its own issuer record. ```json { "name": "azure-prod-tenant", "issuer_url": "https://login.microsoftonline.com//v2.0", "jwks": { "type": "discovery" }, "max_jwt_lifetime_seconds": 7500 } ``` The Connect workload wizard's Microsoft Entra tile creates the issuer with `max_jwt_lifetime_seconds` set to `7500` (just over 2 hours), which covers the default 60 to 90 minute lifetime of `client_credentials` tokens. A tenant token-lifetime policy or Continuous Access Evaluation (CAE) can extend that lifetime. If your decoded token's `exp` minus `iat` exceeds 7500 seconds, edit the issuer in **Settings → Workload identity → Issuers** and raise `max_jwt_lifetime_seconds` to match, or exchanges fail with `invalid_grant`. If your tenant also runs managed-identity workloads from [Use a managed identity](https://platform.claude.com/docs/en/manage-claude/wif-providers/azure#use-a-managed-identity), use that section's `86400` value, which covers both paths. A longer accepted lifetime means a leaked Entra token stays exchangeable for longer. If a token leaks, the lever is disabling the federation rule; a tight `oid` match limits which identities can exchange a token in the first place, as described in [Scope your rule](https://platform.claude.com/docs/en/manage-claude/wif-providers/azure#scope-your-rule). **Federation rule:** Match on the managed identity's object ID and your tenant ID. For the v2.0 tokens this guide configures, the `audience` value is the audience app registration's client ID (the `` GUID from [Register the token audience](https://platform.claude.com/docs/en/manage-claude/wif-providers/azure#register-the-token-audience)). Use the exact `aud` value from your decoded token. ```json { "name": "azure-inference-worker", "issuer_id": "fdis_...", "match": { "audience": "", "claims": { "oid": "9f8e7d6c-1a2b-3c4d-5e6f-...", "tid": "" } }, "target": { "type": "service_account", "service_account_id": "svac_..." }, "workspace_id": "wrkspc_...", "oauth_scope": "workspace:developer", "token_lifetime_seconds": 600 } ``` `token_lifetime_seconds` is the lifetime of the Anthropic access token the exchange returns, not of the Entra token; the SDK refreshes it for you. ### Acquire and use the token At runtime the pod performs the two-hop exchange: it sends the Kubernetes-projected token (the file at `AZURE_FEDERATED_TOKEN_FILE`) to Entra's token endpoint as a federated `client_credentials` assertion, then exchanges the resulting Entra access token at `POST /v1/oauth/token`. Each Anthropic SDK handles the second exchange and the refresh loop when you supply the Entra fetch as a token-provider callable, as shown in the following examples. The cURL tab shows the raw flow. Two different client IDs appear in the samples. `` is the audience app registration's client ID from [Register the token audience](https://platform.claude.com/docs/en/manage-claude/wif-providers/azure#register-the-token-audience); the scope `api:///.default` asks Entra for a token addressed to that audience. `$AZURE_CLIENT_ID` is the managed identity's client ID, injected by the webhook, and identifies the caller. Do not substitute one for the other. If your workload already uses the Azure Identity client library, pass its token acquisition (`DefaultAzureCredential` with the scope `api:///.default`) as the identity token provider instead of performing the two-hop exchange yourself. The library reads the same `AZURE_FEDERATED_TOKEN_FILE`, `AZURE_CLIENT_ID`, and `AZURE_TENANT_ID` environment variables and handles the Entra exchange. ```bash cURL # 1. Exchange the Kubernetes-projected token (at $AZURE_FEDERATED_TOKEN_FILE) # for an Entra-issued JWT. ENTRA_JWT=$(curl -sS "https://login.microsoftonline.com/$AZURE_TENANT_ID/oauth2/v2.0/token" \ -d grant_type=client_credentials \ -d "client_id=$AZURE_CLIENT_ID" \ --data-urlencode "scope=api:///.default" \ -d client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer \ --data-urlencode "client_assertion@$AZURE_FEDERATED_TOKEN_FILE" \ | jq -r .access_token) # 2. Exchange the Entra JWT for an Anthropic access token. ACCESS_TOKEN=$(curl -sS https://api.anthropic.com/v1/oauth/token \ -H "content-type: application/json" \ -d @- < str: federated_token = Path(os.environ["AZURE_FEDERATED_TOKEN_FILE"]).read_text() response = requests.post( f"https://login.microsoftonline.com/{os.environ['AZURE_TENANT_ID']}/oauth2/v2.0/token", data={ "client_id": os.environ["AZURE_CLIENT_ID"], "grant_type": "client_credentials", "scope": "api:///.default", "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", "client_assertion": federated_token, }, timeout=5, ) response.raise_for_status() return response.json()["access_token"] client = anthropic.Anthropic( credentials=WorkloadIdentityCredentials( identity_token_provider=fetch_entra_token_via_federation, federation_rule_id=os.environ["ANTHROPIC_FEDERATION_RULE_ID"], organization_id=os.environ["ANTHROPIC_ORGANIZATION_ID"], service_account_id=os.environ["ANTHROPIC_SERVICE_ACCOUNT_ID"], workspace_id=os.environ.get("ANTHROPIC_WORKSPACE_ID"), ), ) message = client.messages.create( model="claude-opus-5", max_tokens=1024, messages=[{"role": "user", "content": "Hello from Azure"}], ) print(next(block.text for block in message.content if block.type == "text")) ``` ```typescript TypeScript import Anthropic from "@anthropic-ai/sdk"; import { oidcFederationProvider } from "@anthropic-ai/sdk/lib/credentials/oidc-federation"; import { readFile } from "node:fs/promises"; async function fetchEntraTokenViaFederation(): Promise { const federatedToken = await readFile(process.env.AZURE_FEDERATED_TOKEN_FILE!, "utf8"); const response = await fetch( `https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}/oauth2/v2.0/token`, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ client_id: process.env.AZURE_CLIENT_ID!, grant_type: "client_credentials", scope: "api:///.default", client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", client_assertion: federatedToken }) } ); const body = (await response.json()) as { access_token: string }; return body.access_token; } const client = new Anthropic({ credentials: oidcFederationProvider({ identityTokenProvider: fetchEntraTokenViaFederation, federationRuleId: process.env.ANTHROPIC_FEDERATION_RULE_ID!, organizationId: process.env.ANTHROPIC_ORGANIZATION_ID!, serviceAccountId: process.env.ANTHROPIC_SERVICE_ACCOUNT_ID, workspaceId: process.env.ANTHROPIC_WORKSPACE_ID, baseURL: "https://api.anthropic.com", fetch }) }); const message = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, messages: [{ role: "user", content: "Hello from Azure" }] }); for (const block of message.content) { if (block.type === "text") { console.log(block.text); } } ``` ```go Go package main import ( "context" "encoding/json" "fmt" "net/http" "net/url" "os" "strings" "github.com/anthropics/anthropic-sdk-go" "github.com/anthropics/anthropic-sdk-go/option" ) func fetchEntraTokenViaFederation(ctx context.Context) (string, error) { federatedToken, err := os.ReadFile(os.Getenv("AZURE_FEDERATED_TOKEN_FILE")) if err != nil { return "", err } form := url.Values{ "client_id": {os.Getenv("AZURE_CLIENT_ID")}, "grant_type": {"client_credentials"}, "scope": {"api:///.default"}, "client_assertion_type": {"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"}, "client_assertion": {strings.TrimSpace(string(federatedToken))}, } tokenURL := "https://login.microsoftonline.com/" + os.Getenv("AZURE_TENANT_ID") + "/oauth2/v2.0/token" req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenURL, strings.NewReader(form.Encode())) if err != nil { return "", err } req.Header.Set("content-type", "application/x-www-form-urlencoded") resp, err := http.DefaultClient.Do(req) if err != nil { return "", err } defer resp.Body.Close() var body struct { AccessToken string `json:"access_token"` } if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { return "", err } return body.AccessToken, nil } func main() { client := anthropic.NewClient( option.WithFederationTokenProvider(option.IdentityTokenFunc(fetchEntraTokenViaFederation), option.FederationOptions{ FederationRuleID: os.Getenv("ANTHROPIC_FEDERATION_RULE_ID"), OrganizationID: os.Getenv("ANTHROPIC_ORGANIZATION_ID"), ServiceAccountID: os.Getenv("ANTHROPIC_SERVICE_ACCOUNT_ID"), WorkspaceID: os.Getenv("ANTHROPIC_WORKSPACE_ID"), }), ) message, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Hello from Azure")), }, }) if err != nil { panic(err) } for _, block := range message.Content { if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok { fmt.Println(textBlock.Text) break } } } ``` ```java Java IdentityTokenProvider fetchEntraTokenViaFederation = () -> { try { var form = Map.of( "client_id", System.getenv("AZURE_CLIENT_ID"), "grant_type", "client_credentials", "scope", "api:///.default", "client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", "client_assertion", Files.readString(Path.of(System.getenv("AZURE_FEDERATED_TOKEN_FILE")))) .entrySet().stream() .map(entry -> entry.getKey() + "=" + URLEncoder.encode(entry.getValue(), UTF_8)) .collect(Collectors.joining("&")); var request = HttpRequest.newBuilder(URI.create( "https://login.microsoftonline.com/" + System.getenv("AZURE_TENANT_ID") + "/oauth2/v2.0/token")) .header("content-type", "application/x-www-form-urlencoded") .POST(HttpRequest.BodyPublishers.ofString(form)) .build(); var response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()); return new ObjectMapper().readTree(response.body()).get("access_token").asText(); } catch (Exception e) { throw new RuntimeException(e); } }; AnthropicClient client = AnthropicOkHttpClient.builder() .federationTokenProvider( fetchEntraTokenViaFederation, System.getenv("ANTHROPIC_FEDERATION_RULE_ID"), System.getenv("ANTHROPIC_ORGANIZATION_ID"), System.getenv("ANTHROPIC_SERVICE_ACCOUNT_ID"), System.getenv("ANTHROPIC_WORKSPACE_ID")) .build(); var message = client.messages().create(MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .addUserMessage("Hello from Azure") .build()); IO.println(message.content()); ``` ```csharp C# var credentials = new WorkloadIdentityCredentials(new WorkloadIdentityOptions { FederationRuleId = Environment.GetEnvironmentVariable("ANTHROPIC_FEDERATION_RULE_ID")!, OrganizationId = Environment.GetEnvironmentVariable("ANTHROPIC_ORGANIZATION_ID"), ServiceAccountId = Environment.GetEnvironmentVariable("ANTHROPIC_SERVICE_ACCOUNT_ID"), WorkspaceId = Environment.GetEnvironmentVariable("ANTHROPIC_WORKSPACE_ID"), IdentityTokenProvider = new EntraFederationTokenProvider(), }); using var client = new AnthropicOidcClient(credentials); var message = await client.Messages.Create(new() { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [new() { Role = Role.User, Content = "Hello from Azure" }], }); foreach (var block in message.Content) { if (block.Value is TextBlock textBlock) { Console.WriteLine(textBlock.Text); } } class EntraFederationTokenProvider : IIdentityTokenProvider { private static readonly HttpClient Http = new(); public async Task GetIdentityTokenAsync(CancellationToken ct = default) { var federatedToken = await File.ReadAllTextAsync( Environment.GetEnvironmentVariable("AZURE_FEDERATED_TOKEN_FILE")!, ct); var tenantId = Environment.GetEnvironmentVariable("AZURE_TENANT_ID"); var form = new FormUrlEncodedContent(new Dictionary { ["client_id"] = Environment.GetEnvironmentVariable("AZURE_CLIENT_ID")!, ["grant_type"] = "client_credentials", ["scope"] = "api:///.default", ["client_assertion_type"] = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", ["client_assertion"] = federatedToken, }); var response = await Http.PostAsync( $"https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token", form, ct); response.EnsureSuccessStatusCode(); using var json = await JsonDocument.ParseAsync( await response.Content.ReadAsStreamAsync(ct), default, ct); return json.RootElement.GetProperty("access_token").GetString()!; } } ``` ```php PHP use Anthropic\Client; use Anthropic\Credentials\WorkloadIdentityCredentials; function fetchEntraTokenViaFederation(): string { $ch = curl_init('https://login.microsoftonline.com/' . getenv('AZURE_TENANT_ID') . '/oauth2/v2.0/token'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POSTFIELDS => http_build_query([ 'client_id' => getenv('AZURE_CLIENT_ID'), 'grant_type' => 'client_credentials', 'scope' => 'api:///.default', 'client_assertion_type' => 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', 'client_assertion' => file_get_contents(getenv('AZURE_FEDERATED_TOKEN_FILE')), ]), ]); $body = json_decode(curl_exec($ch), true); curl_close($ch); return $body['access_token']; } $client = new Client( credentials: new WorkloadIdentityCredentials( identityTokenProvider: fetchEntraTokenViaFederation(...), federationRuleId: getenv('ANTHROPIC_FEDERATION_RULE_ID'), organizationId: getenv('ANTHROPIC_ORGANIZATION_ID'), serviceAccountId: getenv('ANTHROPIC_SERVICE_ACCOUNT_ID'), workspaceId: getenv('ANTHROPIC_WORKSPACE_ID') ?: null, ), ); $message = $client->messages->create( model: 'claude-opus-5', maxTokens: 1024, messages: [['role' => 'user', 'content' => 'Hello from Azure']], ); $textBlock = array_find($message->content, static fn ($block): bool => $block->type === 'text'); echo $textBlock->text, PHP_EOL; ``` ```ruby Ruby require "anthropic" require "json" require "net/http" def fetch_entra_token_via_federation tenant_id = ENV.fetch("AZURE_TENANT_ID") federated_token = File.read(ENV.fetch("AZURE_FEDERATED_TOKEN_FILE")) response = Net::HTTP.post_form( URI("https://login.microsoftonline.com/#{tenant_id}/oauth2/v2.0/token"), "client_id" => ENV.fetch("AZURE_CLIENT_ID"), "grant_type" => "client_credentials", "scope" => "api:///.default", "client_assertion_type" => "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", "client_assertion" => federated_token ) JSON.parse(response.body).fetch("access_token") end client = Anthropic::Client.new( credentials: Anthropic::WorkloadIdentityCredentials.new( identity_token_provider: -> { fetch_entra_token_via_federation }, federation_rule_id: ENV.fetch("ANTHROPIC_FEDERATION_RULE_ID"), organization_id: ENV.fetch("ANTHROPIC_ORGANIZATION_ID"), service_account_id: ENV.fetch("ANTHROPIC_SERVICE_ACCOUNT_ID"), workspace_id: ENV["ANTHROPIC_WORKSPACE_ID"] ) ) message = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [{role: "user", content: "Hello from Azure"}] ) puts message.content.find { it.type == :text }.text ``` ```bash CLI # 1. Exchange the Kubernetes-projected token for an Entra-issued access # token and write it to a temp file the CLI can read. ANTHROPIC_IDENTITY_TOKEN_FILE=$(mktemp) trap 'rm -f "$ANTHROPIC_IDENTITY_TOKEN_FILE"' EXIT curl -sS "https://login.microsoftonline.com/$AZURE_TENANT_ID/oauth2/v2.0/token" \ -d client_id="$AZURE_CLIENT_ID" \ -d grant_type=client_credentials \ --data-urlencode "scope=api:///.default" \ -d client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer \ --data-urlencode client_assertion@"$AZURE_FEDERATED_TOKEN_FILE" \ | jq -r .access_token > "$ANTHROPIC_IDENTITY_TOKEN_FILE" export ANTHROPIC_IDENTITY_TOKEN_FILE # 2. Call the Claude API. ANTHROPIC_FEDERATION_RULE_ID, # ANTHROPIC_ORGANIZATION_ID, ANTHROPIC_SERVICE_ACCOUNT_ID, and ANTHROPIC_WORKSPACE_ID are read # from the environment. ant messages create \ --model claude-opus-5 \ --max-tokens 1024 \ --message '{role: user, content: "Hello from Azure"}' ``` ### Verify the setup From inside a labeled pod, run the cURL exchange shown in [Acquire and use the token](https://platform.claude.com/docs/en/manage-claude/wif-providers/azure#acquire-and-use-the-token-2) and confirm that `POST /v1/oauth/token` returns a `200` with an `access_token` beginning with `sk-ant-oat01-` and an `expires_in` value in seconds. On `400 invalid_grant`, decode the Entra-issued token from step 1 (see [Troubleshoot a failed exchange](https://platform.claude.com/docs/en/manage-claude/wif-reference#troubleshoot-a-failed-exchange) for the command) and check the most common Azure-side causes: * **Issuer mismatch:** The registered `issuer_url` must match the token's `iss` claim exactly. A v2.0 token carries `https://login.microsoftonline.com//v2.0`; if the decoded `ver` claim is `1.0`, see [If your tokens are v1.0](https://platform.claude.com/docs/en/manage-claude/wif-providers/azure#if-your-tokens-are-v1-0). * **Token lifetime:** If a tenant token-lifetime policy or CAE extends the `client_credentials` token past 7500 seconds, raise the issuer's `max_jwt_lifetime_seconds` as described in [Configure Anthropic](https://platform.claude.com/docs/en/manage-claude/wif-providers/azure#configure-anthropic-2). * **Audience mismatch:** The rule's `audience` must equal the token's `aud` exactly: the audience app registration's client ID for the v2.0 tokens this guide configures. * **Claim name mismatch:** A rule that matches on a claim the token does not carry never passes. v1.0 tokens carry the client ID in `appid`, not `azp`; see [If your tokens are v1.0](https://platform.claude.com/docs/en/manage-claude/wif-providers/azure#if-your-tokens-are-v1-0). ## If your tokens are v1.0 This guide configures the audience app registration with `api.requestedAccessTokenVersion: 2`, so every token it shows is v2.0. If you reuse an existing registration that leaves `requestedAccessTokenVersion` unset, Entra issues v1.0 tokens instead. Decode a sample token and check its `ver` claim; if it is `1.0`, four things change: * **Issuer:** The `iss` claim is `https://sts.windows.net//` instead of `https://login.microsoftonline.com//v2.0`. Register the issuer URL exactly as your token's `iss` claim carries it. The two URLs share the same JWKS, so discovery mode works for either. * **Wizard selector:** Pick **v1 (sts.windows.net)** in the Connect workload wizard's **Token issuer** selector instead of **v2.0 (login.microsoftonline.com)**. * **Audience:** The `aud` claim is the identifier URI you passed as `resource` (for example, `api://`), not the registration's client ID. Set the federation rule's `audience` to the exact `aud` value from your decoded token. * **Client ID claim:** The calling identity's client ID appears in `appid`, not `azp`. The two claims never appear in the same token, so a rule that matches on `azp` never passes against a v1.0 token. The `oid`, `sub`, and `tid` claims carry the same values in both versions, so the rest of this guide applies unchanged. ## Scope your rule A federation rule can match the token's subject with `subject_prefix` in addition to (or instead of) the `claims` map; see [Rule matching semantics](https://platform.claude.com/docs/en/manage-claude/wif-reference#rule-matching-semantics) for how the fields combine. Entra `sub` values for these identities are fixed-length canonical GUIDs, so a `subject_prefix` containing the full 36-character object ID matches only that subject; this is a property of Entra's subject format, not of `subject_prefix` in general. Every identity in your tenant can request a token for the registered audience, so `audience` and `tid` alone do not identify a specific workload. A rule that omits an `oid` (or `azp`/`appid`) match, or that uses a wildcard or partial-GUID `subject_prefix`, authorizes every managed identity and service principal in the tenant. Lock the rule's `match` block to the narrowest scope that fits your use case: * **Match `oid` as an exact value:** Set `claims.oid` to the managed identity's full object ID. A `subject_prefix` set to that full object ID is equivalent (the Console wizard sets both); never use a wildcard or partial-GUID `subject_prefix`, which matches more identities than you intend. * **Pin `tid` as defense in depth:** The issuer URL already pins your tenant, but adding `claims.tid` guards against configuration drift if the issuer record is later edited. * **Pin the audience:** Set `audience` to the exact `aud` value from your decoded token so tokens minted for other applications are rejected. * **Use a separate rule for each managed identity:** Create one rule for each identity rather than one rule that authorizes several, so you can revoke a single workload's access without affecting others. ## Next steps * Review the full configuration model in [Workload Identity Federation](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation). * See the [provider guides](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation#identity-providers) for AWS, Google Cloud, GitHub Actions, and Kubernetes. * For environment variables, profile files, and credential precedence, see the [WIF reference](https://platform.claude.com/docs/en/manage-claude/wif-reference). --- title: Use WIF with Okta url: https://platform.claude.com/docs/en/manage-claude/wif-providers/okta description: Federate Okta service application identities to the Claude API with Workload Identity Federation. --- Okta can act as a workload identity provider by issuing OIDC access tokens to a **service application** through the OAuth 2.0 `client_credentials` grant. Your workload authenticates to Okta (typically with `private_key_jwt`, so no shared secret is stored), receives a signed JSON Web Token (JWT), and exchanges that JWT with Anthropic for a short-lived access token. The Okta authorization server's issuer URL takes the form `https://.okta.com/oauth2/`. If you use the built-in default server, the path is `/oauth2/default`. You must use an Okta **custom authorization server** (including the `default` one). Tokens issued directly by the Okta org authorization server (the `/oauth2/v1/token` endpoint with no authorization server ID in the path) cannot be validated by external parties because Okta does not publish signing keys for them. There are many ways to configure and authenticate to Okta that are outside the scope of this documentation. Ensure that your configuration and authentication mechanisms follow your company's guidance and security practices. ## Prerequisites * Familiarity with [WIF concepts](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation#concepts): service accounts, federation issuers, and federation rules. * An Okta organization with API Access Management enabled (required for custom authorization servers). * Permission to create service accounts, federation issuers, and federation rules in the Claude Console for your Anthropic organization. * A workload that can request a token from Okta's `/v1/token` endpoint and reach `api.anthropic.com`. ## Configure Okta At a high level you need to: 1. Create an Okta service application. 2. Configure your default authorization server (or create a new custom authorization server) with an audience, a scope, an access policy, and any custom claims you want to match on. The exact navigation depends on your Okta org configuration and admin console version. The following numbered steps walk through one common path: 1. **Create a service app integration.** In the Okta Admin Console, create a new app integration of type **API Services** (OIDC, machine-to-machine). Note the generated **Client ID**. 2. **Configure client authentication.** For a keyless setup, choose **Public key / Private key** (`private_key_jwt`) and register your workload's public JWK. Alternatively, use a client secret if your environment can store one securely. For the following example you may need to disable the DPoP requirement on the application; ensure that your production setup adheres to your organization's security requirements. 3. **Set the audience.** On your custom authorization server, set the audience to `https://api.anthropic.com` so issued access tokens carry that `aud` claim. Anthropic validates `aud` against this fixed value. 4. **Grant a scope.** On your custom authorization server, ensure at least one scope exists that the service app is allowed to request (for example, `anthropic.access`). Okta rejects `client_credentials` requests that do not include a granted scope. 5. **Create an access policy.** On your custom authorization server, create an access policy with at least one rule that allows your service app to request the scope you granted in step 4. 6. **(Optional) Add custom claims.** If you want to match on something other than the client ID, add a claim to the access token in your authorization server's **Claims** tab. For a service app using `client_credentials`, Okta sets the `sub` claim of the issued access token to the application's **Client ID**, and `iss` to the authorization server's issuer URL. ## Configure Anthropic In the Claude Console, open **Settings → Workload identity**, click **Connect workload**, and select **Custom OIDC**. The wizard walks you through registering the issuer, creating a service account, and creating a federation rule. The wizard creates these resources for you. Use the following values whether you enter them in the wizard or send them to the [Admin API](https://platform.claude.com/docs/en/manage-claude/wif-admin-api): **Federation issuer:** Use your Okta custom authorization server URL and discovery mode. Anthropic reads Okta's `.well-known/openid-configuration` discovery document and fetches the JWKS from the `jwks_uri` it advertises. ```json { "name": "okta-prod", "issuer_url": "https://acme.okta.com/oauth2/aus1a2b3c4d5e6f7g8h9", "jwks": { "type": "discovery" } } ``` **Federation rule:** Match on the Okta `sub` claim, which is the service app's Client ID. If you defined custom claims in Okta, you can match on those instead with the `claims` map or a CEL `condition`. ```json { "name": "okta-pipeline", "issuer_id": "fdis_...", "match": { "subject_prefix": "0oa1b2c3d4e5f6g7h8i9", "audience": "https://api.anthropic.com" }, "target": { "type": "service_account", "service_account_id": "svac_..." }, "workspace_id": "wrkspc_...", "oauth_scope": "workspace:developer", "token_lifetime_seconds": 600 } ``` ## Acquire a token and call the Claude API Unlike platform-native providers (AWS, Google Cloud, Kubernetes), which make a token available inside the workload's runtime (through a projected file or local metadata endpoint), Okta does not. Your workload must call Okta's token endpoint to obtain a JWT, then pass that JWT to the Anthropic SDK as the identity token. ```bash cURL # 1. Request an access token from Okta (client_credentials with private_key_jwt). OKTA_JWT=$(curl -sS "https://acme.okta.com/oauth2/aus1a2b3c4d5e6f7g8h9/v1/token" \ -d grant_type=client_credentials \ -d scope=anthropic.access \ -d client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer \ --data-urlencode client_assertion="$SIGNED_CLIENT_ASSERTION" \ | jq -r .access_token) # 2. Exchange the Okta JWT for an Anthropic access token. ACCESS_TOKEN=$(curl -sS https://api.anthropic.com/v1/oauth/token \ -H "content-type: application/json" \ -d @- < str: response = httpx.post( f"{os.environ['OKTA_ISSUER']}/v1/token", data={ "grant_type": "client_credentials", "scope": "anthropic.access", "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", # Build the RFC 7523 client_assertion JWT signed with your Okta app's private key "client_assertion": build_signed_client_assertion(), }, ) response.raise_for_status() return response.json()["access_token"] client = anthropic.Anthropic( credentials=WorkloadIdentityCredentials( identity_token_provider=fetch_okta_token, federation_rule_id=os.environ["ANTHROPIC_FEDERATION_RULE_ID"], organization_id=os.environ["ANTHROPIC_ORGANIZATION_ID"], service_account_id=os.environ["ANTHROPIC_SERVICE_ACCOUNT_ID"], workspace_id=os.environ.get("ANTHROPIC_WORKSPACE_ID"), ), ) message = client.messages.create( model="claude-opus-5", max_tokens=1024, messages=[{"role": "user", "content": "Hello, Claude"}], ) print(next(block.text for block in message.content if block.type == "text")) ``` ```typescript TypeScript import Anthropic from "@anthropic-ai/sdk"; import { oidcFederationProvider } from "@anthropic-ai/sdk/lib/credentials/oidc-federation"; async function fetchOktaToken(): Promise { const response = await fetch(`${process.env.OKTA_ISSUER}/v1/token`, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "client_credentials", scope: "anthropic.access", client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", // Build the RFC 7523 client_assertion JWT signed with your Okta app's private key client_assertion: buildSignedClientAssertion() }) }); const body = (await response.json()) as { access_token: string }; return body.access_token; } const client = new Anthropic({ credentials: oidcFederationProvider({ identityTokenProvider: fetchOktaToken, federationRuleId: process.env.ANTHROPIC_FEDERATION_RULE_ID!, organizationId: process.env.ANTHROPIC_ORGANIZATION_ID!, serviceAccountId: process.env.ANTHROPIC_SERVICE_ACCOUNT_ID, workspaceId: process.env.ANTHROPIC_WORKSPACE_ID, baseURL: "https://api.anthropic.com", fetch }) }); const message = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, messages: [{ role: "user", content: "Hello, Claude" }] }); for (const block of message.content) { if (block.type === "text") { console.log(block.text); } } ``` ```go Go package main import ( "context" "encoding/json" "fmt" "net/http" "net/url" "os" "strings" "github.com/anthropics/anthropic-sdk-go" "github.com/anthropics/anthropic-sdk-go/option" ) func fetchOktaToken(ctx context.Context) (string, error) { form := url.Values{ "grant_type": {"client_credentials"}, "scope": {"anthropic.access"}, "client_assertion_type": {"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"}, // Build the RFC 7523 client_assertion JWT signed with your Okta app's private key "client_assertion": {buildSignedClientAssertion()}, } req, err := http.NewRequestWithContext(ctx, http.MethodPost, os.Getenv("OKTA_ISSUER")+"/v1/token", strings.NewReader(form.Encode())) if err != nil { return "", err } req.Header.Set("content-type", "application/x-www-form-urlencoded") resp, err := http.DefaultClient.Do(req) if err != nil { return "", err } defer resp.Body.Close() var body struct { AccessToken string `json:"access_token"` } if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { return "", err } return body.AccessToken, nil } func main() { client := anthropic.NewClient( option.WithFederationTokenProvider(option.IdentityTokenFunc(fetchOktaToken), option.FederationOptions{ FederationRuleID: os.Getenv("ANTHROPIC_FEDERATION_RULE_ID"), OrganizationID: os.Getenv("ANTHROPIC_ORGANIZATION_ID"), ServiceAccountID: os.Getenv("ANTHROPIC_SERVICE_ACCOUNT_ID"), WorkspaceID: os.Getenv("ANTHROPIC_WORKSPACE_ID"), }), ) message, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Hello, Claude")), }, }) if err != nil { panic(err) } for _, block := range message.Content { if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok { fmt.Println(textBlock.Text) break } } } ``` ```java Java IdentityTokenProvider fetchOktaToken = () -> { try { var form = Map.of( "grant_type", "client_credentials", "scope", "anthropic.access", "client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", // Build the RFC 7523 client_assertion JWT signed with your Okta app's private key "client_assertion", buildSignedClientAssertion()) .entrySet().stream() .map(entry -> entry.getKey() + "=" + URLEncoder.encode(entry.getValue(), UTF_8)) .collect(Collectors.joining("&")); var request = HttpRequest.newBuilder(URI.create(System.getenv("OKTA_ISSUER") + "/v1/token")) .header("content-type", "application/x-www-form-urlencoded") .POST(HttpRequest.BodyPublishers.ofString(form)) .build(); var response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()); return new ObjectMapper().readTree(response.body()).get("access_token").asText(); } catch (Exception e) { throw new RuntimeException(e); } }; AnthropicClient client = AnthropicOkHttpClient.builder() .federationTokenProvider( fetchOktaToken, System.getenv("ANTHROPIC_FEDERATION_RULE_ID"), System.getenv("ANTHROPIC_ORGANIZATION_ID"), System.getenv("ANTHROPIC_SERVICE_ACCOUNT_ID")) .build(); var message = client.messages().create(MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024) .addUserMessage("Hello, Claude") .build()); IO.println(message.content()); ``` ```csharp C# var credentials = new WorkloadIdentityCredentials(new WorkloadIdentityOptions { FederationRuleId = Environment.GetEnvironmentVariable("ANTHROPIC_FEDERATION_RULE_ID")!, OrganizationId = Environment.GetEnvironmentVariable("ANTHROPIC_ORGANIZATION_ID"), ServiceAccountId = Environment.GetEnvironmentVariable("ANTHROPIC_SERVICE_ACCOUNT_ID"), WorkspaceId = Environment.GetEnvironmentVariable("ANTHROPIC_WORKSPACE_ID"), IdentityTokenProvider = new OktaTokenProvider(), }); using var client = new AnthropicOidcClient(credentials); var message = await client.Messages.Create(new() { Model = Model.ClaudeOpus5, MaxTokens = 1024, Messages = [new() { Role = Role.User, Content = "Hello, Claude" }], }); foreach (var block in message.Content) { if (block.Value is TextBlock textBlock) { Console.WriteLine(textBlock.Text); } } class OktaTokenProvider : IIdentityTokenProvider { private static readonly HttpClient Http = new(); public async Task GetIdentityTokenAsync(CancellationToken ct = default) { var form = new FormUrlEncodedContent(new Dictionary { ["grant_type"] = "client_credentials", ["scope"] = "anthropic.access", ["client_assertion_type"] = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", // Build the RFC 7523 client_assertion JWT signed with your Okta app's private key ["client_assertion"] = BuildSignedClientAssertion(), }); var response = await Http.PostAsync( $"{Environment.GetEnvironmentVariable("OKTA_ISSUER")}/v1/token", form, ct); response.EnsureSuccessStatusCode(); using var json = await JsonDocument.ParseAsync( await response.Content.ReadAsStreamAsync(ct), default, ct); return json.RootElement.GetProperty("access_token").GetString()!; } } ``` ```bash CLI # 1. Request an access token from Okta and write it to a temp file. ANTHROPIC_IDENTITY_TOKEN_FILE=$(mktemp) curl -sS "$OKTA_ISSUER/v1/token" \ -d grant_type=client_credentials \ -d scope=anthropic.access \ -d client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer \ --data-urlencode client_assertion="$SIGNED_CLIENT_ASSERTION" \ | jq -r .access_token > "$ANTHROPIC_IDENTITY_TOKEN_FILE" export ANTHROPIC_IDENTITY_TOKEN_FILE # 2. Call the Claude API. The CLI reads ANTHROPIC_FEDERATION_RULE_ID, # ANTHROPIC_ORGANIZATION_ID, ANTHROPIC_SERVICE_ACCOUNT_ID, ANTHROPIC_WORKSPACE_ID, and # ANTHROPIC_IDENTITY_TOKEN_FILE and performs the exchange. ant messages create \ --model claude-opus-5 \ --max-tokens 1024 \ --message '{role: user, content: "Hello, Claude"}' ``` ```php PHP use Anthropic\Client; use Anthropic\Credentials\WorkloadIdentityCredentials; function fetchOktaToken(): string { $ch = curl_init(getenv('OKTA_ISSUER') . '/v1/token'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POSTFIELDS => http_build_query([ 'grant_type' => 'client_credentials', 'scope' => 'anthropic.access', 'client_assertion_type' => 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', // Build the RFC 7523 client_assertion JWT signed with your Okta app's private key 'client_assertion' => buildSignedClientAssertion(), ]), ]); $body = json_decode(curl_exec($ch), true); curl_close($ch); return $body['access_token']; } $client = new Client( credentials: new WorkloadIdentityCredentials( identityTokenProvider: fetchOktaToken(...), federationRuleId: getenv('ANTHROPIC_FEDERATION_RULE_ID'), organizationId: getenv('ANTHROPIC_ORGANIZATION_ID'), serviceAccountId: getenv('ANTHROPIC_SERVICE_ACCOUNT_ID'), workspaceId: getenv('ANTHROPIC_WORKSPACE_ID') ?: null, ), ); $message = $client->messages->create( model: 'claude-opus-5', maxTokens: 1024, messages: [['role' => 'user', 'content' => 'Hello, Claude']], ); echo array_find($message->content, static fn ($block): bool => $block->type === 'text')->text, PHP_EOL; ``` ```ruby Ruby require "anthropic" require "json" require "net/http" def fetch_okta_token uri = URI("#{ENV.fetch('OKTA_ISSUER')}/v1/token") response = Net::HTTP.post_form( uri, "grant_type" => "client_credentials", "scope" => "anthropic.access", "client_assertion_type" => "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", # Build the RFC 7523 client_assertion JWT signed with your Okta app's private key "client_assertion" => build_signed_client_assertion ) JSON.parse(response.body).fetch("access_token") end client = Anthropic::Client.new( credentials: Anthropic::WorkloadIdentityCredentials.new( identity_token_provider: -> { fetch_okta_token }, federation_rule_id: ENV.fetch("ANTHROPIC_FEDERATION_RULE_ID"), organization_id: ENV.fetch("ANTHROPIC_ORGANIZATION_ID"), service_account_id: ENV.fetch("ANTHROPIC_SERVICE_ACCOUNT_ID"), workspace_id: ENV["ANTHROPIC_WORKSPACE_ID"] ) ) message = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [{role: "user", content: "Hello, Claude"}] ) puts message.content.find { it.type == :text }.text ``` Each SDK tab shows the callable pattern: the Anthropic SDK calls your identity-token provider again whenever the Anthropic access token approaches expiry, so your Okta fetcher should return a fresh token on each call rather than caching one indefinitely. The `ant` CLI re-reads `ANTHROPIC_IDENTITY_TOKEN_FILE` on each exchange, so refresh that file on a timer for long-running shells. ## Verify the setup A successful exchange returns an `access_token` beginning with `sk-ant-oat01-` and an `expires_in` value in seconds. On `400 invalid_grant`, see [Troubleshoot a failed exchange](https://platform.claude.com/docs/en/manage-claude/wif-reference#troubleshoot-a-failed-exchange); the most common Okta-side cause is an `issuer_url` mismatch (it must include the `/oauth2/` path; the Okta org authorization server is not usable). ## Scope your rule Multiple service apps under the same Okta authorization server share the same issuer. A rule that omits `subject_prefix` matches every service app on that server, so any team that can register one could obtain a federated Anthropic token. Lock the rule's `match` block to the narrowest scope that fits your use case: * **Pin the exact Client ID:** Set `subject_prefix` to the service app's full Client ID with no trailing `*`. * **Pin the audience:** Match the `audience` value you configured on the authorization server so tokens minted for a different audience are rejected. * **Match on custom claims:** For finer-grained scoping, add claims in the authorization server's **Claims** tab and match them with the rule's `claims` map or a CEL `condition`. * **Use one rule per service app:** Create a separate federation rule for each service app rather than sharing one rule across apps. ## Next steps * Review the [WIF reference](https://platform.claude.com/docs/en/manage-claude/wif-reference) for the full credential resolution order and profile configuration. * See the [WIF reference](https://platform.claude.com/docs/en/manage-claude/wif-reference#rule-matching-semantics) to match on custom Okta claims with CEL expressions. --- title: Use WIF with SPIFFE url: https://platform.claude.com/docs/en/manage-claude/wif-providers/spiffe description: Authenticate SPIFFE workloads to the Claude API using JWT-SVIDs from SPIRE or any other SPIFFE-conformant issuer. --- [SPIFFE](https://spiffe.io/) is the CNCF standard for issuing identity to workloads. [SPIRE](https://spiffe.io/docs/latest/spire-about/) is its open-source reference implementation, and several commercial products also issue SPIFFE-conformant identities. Anthropic federates with any SPIFFE implementation that emits OIDC-compatible JWT-SVIDs. For a current list of implementations, see [Commercial software that implements SPIFFE](https://spiffe.io/docs/latest/spiffe-about/overview/#commercial-software-that-implements-spiffe) on the SPIFFE project site. Federation works either through an OIDC discovery document at a public HTTPS URL (`discovery` mode, subject to the [URL constraints](https://platform.claude.com/docs/en/manage-claude/wif-reference#url-fields)) or by registering the JWKS directly (`inline` mode). The JWT-SVID spec defines `sub` as the workload's SPIFFE ID, and the SPIFFE Workload API requires the caller to supply `aud` at fetch time, so those claims are the same across implementations. Anthropic additionally requires `iss` and `iat`, neither of which the JWT-SVID spec mandates, so configure your implementation to populate both (in SPIRE, `iss` is the `jwt_issuer` server setting and `iat` is set automatically). With those in place, the [Configure Anthropic](https://platform.claude.com/docs/en/manage-claude/wif-providers/spiffe#configure-anthropic), [Acquire and use the token](https://platform.claude.com/docs/en/manage-claude/wif-providers/spiffe#acquire-and-use-the-token), and [Scope your rule](https://platform.claude.com/docs/en/manage-claude/wif-providers/spiffe#scope-your-rule) sections of this guide apply to any SPIFFE implementation. SPIFFE assigns every workload a stable identity URI of the form `spiffe:///`, and SPIRE issues that identity as a JWT-SVID on demand through the Workload API. A JWT-SVID is an ordinary signed JWT whose `sub` claim is the workload's SPIFFE ID and whose `aud` claim is supplied by the workload at fetch time. The bridge from a SPIRE trust domain to standard OIDC is the [SPIRE OIDC Discovery Provider](https://github.com/spiffe/spire/blob/main/support/oidc-discovery-provider/README.md), a standalone helper that publishes `/.well-known/openid-configuration` and a JWKS endpoint for the trust domain's JWT signing keys. With the discovery provider running, a JWT-SVID validates like any other OIDC token: register the discovery URL as a federation issuer, write a federation rule that matches the workload's SPIFFE ID, and have the workload present its JWT-SVID to Anthropic's token-exchange endpoint. This page's examples use SPIRE and apply anywhere SPIRE Agent runs: Kubernetes pods, virtual machines, and bare-metal hosts. If your Kubernetes cluster does not run SPIRE and you want to authenticate with the cluster's native projected service-account tokens instead, see [Use WIF with Kubernetes](https://platform.claude.com/docs/en/manage-claude/wif-providers/kubernetes). ## Prerequisites * Familiarity with [WIF concepts](https://platform.claude.com/docs/en/manage-claude/workload-identity-federation#concepts): service accounts, federation issuers, and federation rules. * A SPIFFE deployment with workload identities issued (the examples on this page use SPIRE Server and Agent), and registration entries for the workloads that need to call the Claude API. * An OIDC discovery endpoint for the trust domain (in SPIRE, the [OIDC Discovery Provider](https://github.com/spiffe/spire/blob/main/support/oidc-discovery-provider/README.md)) running with a publicly reachable HTTPS endpoint, or the JWKS exported for `inline` registration. * Your SPIFFE issuer configured to set the `iss` claim on JWT-SVIDs to the value you will register as the federation issuer's `issuer_url`. For `discovery` mode, this is the discovery endpoint's public URL (in SPIRE, the `jwt_issuer` server setting). * JWT-SVIDs available to your workloads. WIF accepts JWT-SVIDs only, not X.509-SVIDs. * Permission to create service accounts, federation issuers, and federation rules in the Claude Console for your Anthropic organization. The audience value to request when fetching a JWT-SVID is always `https://api.anthropic.com`. Use this value in spiffe-helper's `jwt_audience`, the Workload API `FetchJWTSVID` call, and the federation rule's `audience` matcher. ## Configure SPIRE The instructions in this section are SPIRE-specific. If you use a different SPIFFE issuer, configure its OIDC discovery endpoint and JWT-SVID retrieval according to its own documentation, then continue at [Configure Anthropic](https://platform.claude.com/docs/en/manage-claude/wif-providers/spiffe#configure-anthropic). If you already run SPIRE with the OIDC Discovery Provider, federating with Anthropic requires three things on the SPIRE side: a `jwt_issuer` that matches the discovery URL, a registration entry for the workload that will call the Claude API, and a way for that workload to fetch a JWT-SVID with the Anthropic audience. The following subsections walk through each. The configuration snippets show only the settings relevant to Anthropic federation, not complete SPIRE deployment configs. Setting up SPIRE for the first time? Deploy SPIRE Server and Agent following the [SPIRE quickstart](https://spiffe.io/docs/latest/try/), then add the [OIDC Discovery Provider](https://github.com/spiffe/spire/blob/main/support/oidc-discovery-provider/README.md) as a separate service alongside SPIRE Server. Discovery-mode federation depends on the provider being deployed and publicly reachable. The provider is not part of a default SPIRE install. ### Verify the JWT issuer Anthropic validates a JWT-SVID by matching its `iss` claim against a registered federation issuer and fetching the JWKS from that issuer's discovery document. Two SPIRE settings must agree on the same URL: SPIRE Server's `jwt_issuer` (which becomes the `iss` claim in every minted JWT-SVID) and the OIDC Discovery Provider's `domains` list (which determines the host the discovery document and JWKS are served from). That shared URL is what you register with Anthropic. The trust domain and the issuer URL are independent. The trust domain (`spiffe://prod.example.com`) scopes the `sub` claim. The issuer URL (`https://oidc-discovery.prod.example.com`) is where Anthropic fetches signing keys. They do not need to share a hostname. Confirm `jwt_issuer` is set in SPIRE Server's configuration and points at the discovery provider's public URL. The following example also shows a default JWT-SVID lifetime. SPIRE's built-in default is 5 minutes, which is short enough that continuous rotation is required (see [Run spiffe-helper](https://platform.claude.com/docs/en/manage-claude/wif-providers/spiffe#run-spiffe-helper)). Anthropic's token-exchange endpoint rejects any identity token whose lifetime exceeds the federation issuer's configured maximum, which is 1 hour by default (see [Validation rules](https://platform.claude.com/docs/en/manage-claude/wif-reference#validation-rules)). This check applies to every SPIFFE implementation, not only SPIRE, so keep `default_jwt_svid_ttl` (or any per-entry override) at or below that maximum. ```text server.conf server { trust_domain = "prod.example.com" jwt_issuer = "https://oidc-discovery.prod.example.com" default_jwt_svid_ttl = "5m" # ... } ``` In the OIDC Discovery Provider's configuration, the same hostname must appear under `domains`, and the provider must be able to reach SPIRE Server's API socket. The provider serves the discovery document and JWKS over HTTPS. Terminate TLS with its built-in ACME support, or front it with a load balancer that does. ```text oidc-discovery-provider.conf domains = ["oidc-discovery.prod.example.com"] server_api { address = "unix:///run/spire/sockets/private/api.sock" } acme { email = "platform@example.com" tos_accepted = true } ``` The example uses `server_api`, which connects the discovery provider to SPIRE Server's privileged API socket. The provider also accepts a `workload_api` block (with `socket_path` and `trust_domain`) that obtains the bundle through a SPIRE Agent's Workload API instead. Use it when the discovery provider should not have access to the Server API or runs on a node that cannot reach the Server. ### Register the workload Each workload that calls the Claude API needs a SPIRE registration entry that maps its runtime selectors to a SPIFFE ID. If the workload is already registered, note its SPIFFE ID, which you use in the federation rule's `subject_prefix`. If not, register it. For a Kubernetes pod, the selectors are typically the namespace and Kubernetes service account: ```bash CLI # Replace NODE_UID with the node's UID: # kubectl get node -o jsonpath='{.metadata.uid}' spire-server entry create \ -spiffeID spiffe://prod.example.com/ns/inference/sa/worker \ -parentID spiffe://prod.example.com/spire/agent/k8s_psat/prod-cluster/NODE_UID \ -selector k8s:ns:inference \ -selector k8s:sa:worker ``` The `parentID` shown is a single node's auto-generated agent ID. For cluster-wide registration, parent the entry to a [node alias](https://spiffe.io/docs/latest/deploying/registering/#mapping-workloads-to-multiple-nodes) so it matches workloads on every node, as the [SPIRE Kubernetes quickstart](https://spiffe.io/docs/latest/try/getting-started-k8s/) does. Workloads outside Kubernetes use host-level selectors such as `unix:uid:1000` (`unix:path` is also available but requires `discover_workload_path = true` in the agent's unix workload attestor configuration). Clusters running [spire-controller-manager](https://github.com/spiffe/spire-controller-manager) can declare entries with the `ClusterSPIFFEID` custom resource instead of calling `spire-server entry create` directly. ### Run spiffe-helper [spiffe-helper](https://github.com/spiffe/spiffe-helper) is a sidecar utility that connects to the SPIRE Agent socket, fetches a JWT-SVID for a given audience, writes it to a file, and re-fetches it before expiry. The helper runs in daemon mode by default. The following example sets `daemon_mode = true` explicitly. ```text helper.conf agent_address = "/run/spire/sockets/agent.sock" # The JWT-SVID file is written under cert_dir cert_dir = "/var/run/secrets/anthropic.com" daemon_mode = true jwt_svids = [{ jwt_audience = "https://api.anthropic.com" jwt_svid_file_name = "token" }] ``` In Kubernetes, run spiffe-helper as a sidecar container that shares a memory-backed `emptyDir` volume (`medium: Memory`) with your application container so the bearer SVID never lands on the node's disk. Mount the SPIRE Agent socket from the host into the sidecar, mount the shared volume at `/var/run/secrets/anthropic.com` in both containers, and set `ANTHROPIC_IDENTITY_TOKEN_FILE=/var/run/secrets/anthropic.com/token` on the application container. On VMs and bare metal, run spiffe-helper as a system service alongside the workload and point both at a shared directory. ## Configure Anthropic In the Claude Console, open **Settings → Workload identity**, click **Connect workload**, and select **Custom OIDC**. The wizard walks you through registering the issuer, creating a service account, and creating a federation rule. The wizard creates these resources for you. Use the following values whether you enter them in the wizard or send them to the [Admin API](https://platform.claude.com/docs/en/manage-claude/wif-admin-api): **Federation issuer:** Register the OIDC Discovery Provider's public URL in `discovery` mode. Anthropic fetches `/.well-known/openid-configuration` from this URL and follows the returned `jwks_uri` to retrieve the trust domain's signing keys. ```json { "name": "spire-prod", "issuer_url": "https://oidc-discovery.prod.example.com", "jwks": { "type": "discovery" } } ``` If the discovery provider is not reachable from the public internet, fetch the JWKS yourself (`curl https://oidc-discovery.prod.example.com/keys`) and register the issuer with `"jwks": {"type": "inline", "keys": [...]}` using the contents of the returned `keys` array. In `inline` mode the `issuer_url` is only compared against the JWT-SVID's `iss` claim. Anthropic never attempts to reach it. SPIRE rotates JWT signing keys frequently, by default on the same cadence as the CA (`ca_ttl`, 24 hours). If you register the issuer with an inline JWKS instead of a discovery URL, you must update the JWKS every time SPIRE rotates: add the new key before workloads start presenting it, and **remove superseded keys** once tokens signed with them have expired. Stale keys left in an inline JWKS remain trusted indefinitely. To automate JWKS updates without exposing a public discovery endpoint, configure a SPIRE Server [BundlePublisher](https://spiffe.io/docs/latest/deploying/spire_server/#built-in-plugins) plugin (`aws_s3`, `gcp_cloudstorage`, or `k8s_configmap`) with `format = "jwks"` to push the JWT signing keys to external storage on every rotation, then update the issuer's inline keys through the [Admin API](https://platform.claude.com/docs/en/manage-claude/wif-admin-api#federation-issuers). **Federation rule:** Match the JWT-SVID's `sub` (the SPIFFE ID) and the `aud` you configured spiffe-helper to request. SPIFFE IDs are URI strings and `subject_prefix` matches them as opaque text, so an exact value or a trailing-`*` prefix match both work against them. For more complex patterns, use a CEL `condition`. ```json { "name": "spire-inference-worker", "issuer_id": "fdis_...", "match": { "subject_prefix": "spiffe://prod.example.com/ns/inference/sa/worker", "audience": "https://api.anthropic.com" }, "target": { "type": "service_account", "service_account_id": "svac_..." }, "workspace_id": "wrkspc_...", "oauth_scope": "workspace:developer", "token_lifetime_seconds": 600 } ``` `token_lifetime_seconds` is the lifetime of the Anthropic access token the exchange returns, not of the JWT-SVID. The SDK refreshes the access token automatically. Be as specific as the workload allows. Loosen `subject_prefix` to `spiffe://prod.example.com/ns/inference/*` only if every workload registered under that path should map to the same Anthropic service account. Add the rule's `fdrl_...` ID to the workload's `ANTHROPIC_FEDERATION_RULE_ID` environment variable. ## Acquire and use the token The Anthropic SDKs can either read the JWT-SVID from the file that spiffe-helper maintains or call the SPIFFE Workload API directly through a token-provider callable. The file path is the simplest integration and works in every SDK language. The callable path removes the sidecar but requires a SPIFFE Workload API client in your application's language. With spiffe-helper writing a fresh JWT-SVID to `/var/run/secrets/anthropic.com/token`, set `ANTHROPIC_IDENTITY_TOKEN_FILE` to that path along with `ANTHROPIC_FEDERATION_RULE_ID`, `ANTHROPIC_ORGANIZATION_ID`, `ANTHROPIC_SERVICE_ACCOUNT_ID`, and `ANTHROPIC_WORKSPACE_ID`. The SDK reads the file on every token exchange, so it always picks up the most recently rotated SVID, and refreshes the Anthropic access token automatically before it expires. See [Environment variables](https://platform.claude.com/docs/en/manage-claude/wif-reference#environment-variables) for where each value comes from. ```bash cURL JWT=$(cat "$ANTHROPIC_IDENTITY_TOKEN_FILE") ACCESS_TOKEN=$(curl -sS https://api.anthropic.com/v1/oauth/token \ -H "content-type: application/json" \ --data @- <messages->create( model: 'claude-opus-5', maxTokens: 1024, messages: [['role' => 'user', 'content' => 'Hello, Claude']], ); $textBlock = array_find($message->content, static fn ($block): bool => $block->type === 'text'); echo $textBlock->text, PHP_EOL; ``` ```ruby Ruby require "anthropic" # Reads the JWT-SVID that spiffe-helper writes to # ANTHROPIC_IDENTITY_TOKEN_FILE, plus ANTHROPIC_FEDERATION_RULE_ID, # ANTHROPIC_ORGANIZATION_ID, ANTHROPIC_SERVICE_ACCOUNT_ID, and ANTHROPIC_WORKSPACE_ID. client = Anthropic::Client.new message = client.messages.create( model: "claude-opus-5", max_tokens: 1024, messages: [{role: "user", content: "Hello, Claude"}] ) puts message.content.find { it.type == :text }.text ``` Workloads that link a SPIFFE Workload API client directly can skip spiffe-helper and pass the SDK a callable that fetches a fresh JWT-SVID from the agent socket. The SDK invokes the callable before each token exchange, so the workload always presents an unexpired SVID. Python ([py-spiffe](https://github.com/HewlettPackard/py-spiffe)) and Go ([go-spiffe](https://github.com/spiffe/go-spiffe)) have mature Workload API clients. ```python Python import os import anthropic from anthropic import WorkloadIdentityCredentials from spiffe import JwtSource AUDIENCE = "https://api.anthropic.com" # Connects to the SPIRE Agent socket at SPIFFE_ENDPOINT_SOCKET. jwt_source = JwtSource() def fetch_jwt_svid() -> str: svid = jwt_source.fetch_svid(audience={AUDIENCE}) # audience is a set of strings return svid.token client = anthropic.Anthropic( credentials=WorkloadIdentityCredentials( identity_token_provider=fetch_jwt_svid, federation_rule_id=os.environ["ANTHROPIC_FEDERATION_RULE_ID"], organization_id=os.environ["ANTHROPIC_ORGANIZATION_ID"], service_account_id=os.environ["ANTHROPIC_SERVICE_ACCOUNT_ID"], workspace_id=os.environ.get("ANTHROPIC_WORKSPACE_ID"), ), ) message = client.messages.create( model="claude-opus-5", max_tokens=1024, messages=[{"role": "user", "content": "Hello, Claude"}], ) print(next(block.text for block in message.content if block.type == "text")) ``` ```go Go import ( "context" "fmt" "os" "github.com/anthropics/anthropic-sdk-go" "github.com/anthropics/anthropic-sdk-go/option" "github.com/spiffe/go-spiffe/v2/svid/jwtsvid" "github.com/spiffe/go-spiffe/v2/workloadapi" ) // ... const audience = "https://api.anthropic.com" ctx := context.Background() source, err := workloadapi.NewJWTSource(ctx) if err != nil { panic(err) } defer source.Close() fetchJWTSVID := func(ctx context.Context) (string, error) { svid, err := source.FetchJWTSVID(ctx, jwtsvid.Params{Audience: audience}) if err != nil { return "", err } return svid.Marshal(), nil } client := anthropic.NewClient( option.WithFederationTokenProvider(fetchJWTSVID, option.FederationOptions{ FederationRuleID: os.Getenv("ANTHROPIC_FEDERATION_RULE_ID"), OrganizationID: os.Getenv("ANTHROPIC_ORGANIZATION_ID"), ServiceAccountID: os.Getenv("ANTHROPIC_SERVICE_ACCOUNT_ID"), WorkspaceID: os.Getenv("ANTHROPIC_WORKSPACE_ID"), }), ) message, err := client.Messages.New(ctx, anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Hello, Claude")), }, }) if err != nil { panic(err) } for _, block := range message.Content { if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok { fmt.Println(textBlock.Text) break } } ``` For other languages, fetch the JWT-SVID with your runtime's SPIFFE Workload API client (or shell out to `spire-agent api fetch jwt`), write it to a file, and set `ANTHROPIC_IDENTITY_TOKEN_FILE` to that path as in the file-based tab. ## Verify the setup Before wiring the SDK in, fetch a JWT-SVID directly from SPIRE Agent and confirm the claims match what your federation rule expects. If you use a different SPIFFE implementation, fetch a JWT-SVID with its CLI or Workload API client and decode the payload the same way. The Workload API attests the calling process. For a Kubernetes registration entry, run this command inside a pod that satisfies the entry's selectors and has the agent socket mounted (for example, by using `kubectl exec`). On VMs and bare metal, run it as the user or process that matches the entry's `unix:` selectors. Running from an unattested host shell returns `no identity issued`, which is the most common verify-step failure. ```bash CLI spire-agent api fetch jwt \ -audience https://api.anthropic.com \ -socketPath /run/spire/sockets/agent.sock \ -output json \ | jq -r '.[0].svids[0].svid' \ | jq -rR 'split(".")[1] | gsub("-";"+") | gsub("_";"/") | @base64d | fromjson' ``` The `-output json` flag returns the SVID response and bundle response as a two-element JSON array, so `jq -r '.[0].svids[0].svid'` extracts the bare token. On older SPIRE versions without `-output`, the command prints a labeled block instead. In that case, pipe the default output through `awk '/^[[:space:]]*eyJ/{print $1; exit}'` to extract the token line. Check that `iss` is the OIDC Discovery Provider URL you registered, `sub` is the workload's SPIFFE ID, and `aud` contains `https://api.anthropic.com`. Then run the cURL example from [Acquire and use the token](https://platform.claude.com/docs/en/manage-claude/wif-providers/spiffe#acquire-and-use-the-token). A successful exchange returns an `access_token` beginning with `sk-ant-oat01-`. On `400 invalid_grant`, see [Troubleshoot a failed exchange](https://platform.claude.com/docs/en/manage-claude/wif-reference#troubleshoot-a-failed-exchange). The most common SPIRE-side cause is a mismatch between SPIRE Server's `jwt_issuer` and the URL registered as the federation issuer. ## Scope your rule SPIFFE ID path conventions are operator-defined, so the federation rule's `subject_prefix` matcher should reflect the path scheme your registration entries use. Common schemes include `spiffe:///ns//sa/` (the default emitted by the `ClusterSPIFFEID` resource in spire-controller-manager) and `spiffe:///host//` for VM and bare-metal workloads. A `subject_prefix` of `spiffe://prod.example.com/*` matches every workload in the trust domain. Without an `audience` matcher, the rule also accepts JWT-SVIDs minted for any audience, including ones the workload requested for unrelated relying parties. Lock the rule's `match` block to the narrowest scope that fits your use case: * **Pin to one workload:** Set `subject_prefix` to the full SPIFFE ID with no trailing `*`. * **Always set an audience:** Require `audience` on the rule and configure spiffe-helper (or the Workload API call) with the same value so SVIDs minted for other relying parties are rejected. * **Scope by path segment:** Use `spiffe://prod.example.com/ns/inference/*` to grant every workload registered under a namespace, and create a separate rule and Anthropic service account per namespace rather than widening one rule. * **One issuer per trust domain:** Each SPIRE trust domain has its own signing keys and OIDC Discovery Provider. Register each as a separate federation issuer and bind rules to the issuer that owns the SPIFFE IDs they match. ## Next steps Federate Okta service application identities to the Claude API with Workload Identity Federation. Authenticate workloads to the Claude API with short-lived identity tokens from your own identity provider instead of long-lived static API keys. Environment variables, validation rules, profile configuration, and error reference for Workload Identity Federation. Authenticate to the Claude API from self-managed Kubernetes clusters using projected service account tokens. ### Monitoring --- title: Analytics APIs url: https://platform.claude.com/docs/en/manage-claude/analytics-api description: Understand which analytics API and API key your organization needs, then provision access to Claude Code productivity metrics or Claude Enterprise engagement and adoption data. --- Anthropic provides two analytics APIs, and which one you use depends on which Claude product your organization manages: * The **Claude Code Analytics API** reports daily Claude Code productivity metrics for organizations that use the Claude Platform. It is part of the [Admin API](https://platform.claude.com/docs/en/manage-claude/admin-api) and uses an Admin API key. * The **Claude Enterprise Analytics API** reports organization-wide engagement, adoption, and cost data across Claude products (chat, projects, Claude Code, and more) for Claude Enterprise organizations. It uses an Analytics API key created in claude.ai. The two APIs use different key types, created in different places by different roles. This page describes which API fits your organization and how to create the right key. ## Which API do you need? | API | Key type | Created in | Who can create it | What it covers | | ----------------------------------- | ------------------------------------ | ----------------------------------------------------------------------------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- | | **Claude Code Analytics API** | Admin API key (`sk-ant-admin01-...`) | [Claude Console > Settings > Admin keys](https://platform.claude.com/settings/admin-keys) | Organization admin | Daily Claude Code metrics per user: sessions, lines of code, commits, pull requests, tool acceptance, and estimated cost by model | | **Claude Enterprise Analytics API** | Analytics API key | [claude.ai > Organization settings > API](https://claude.ai/admin-settings/api-access) | Primary owner | Organization-wide engagement and adoption (user activity, active-user summaries, project, skill, and connector usage), plus cost and usage reports | The key types are not interchangeable: an Admin API key cannot call the Claude Enterprise Analytics API, and an Analytics API key cannot call the Admin API. Both APIs appear under the [Admin API reference](https://platform.claude.com/docs/en/api/admin), but they are separate APIs with separate key types. If your organization uses both the Claude Platform and Claude Enterprise, you can provision both keys and use each API for its own data. Looking for API usage and cost data rather than product analytics? See the [Usage and Cost API](https://platform.claude.com/docs/en/manage-claude/usage-cost-api), which explains the right path for both Claude Console and Claude Enterprise organizations. If you want to view engagement and adoption data in the product rather than programmatically, use the [Analytics dashboard](https://claude.ai/analytics/activity) in claude.ai. For governance and auditing use cases (individual user actions, raw activity events, conversation content), see the [Compliance API](https://platform.claude.com/docs/en/manage-claude/compliance-api-access). ## Get access to the Claude Code Analytics API The Claude Code Analytics API is available to every organization with access to the [Admin API](https://platform.claude.com/docs/en/manage-claude/admin-api), and is free to use. Follow the steps in [Create an Admin API key](https://platform.claude.com/docs/en/manage-claude/admin-api-keys#create-a-key-for-a-claude-console-organization). Pass the key in the `x-api-key` header: ```bash curl "https://api.anthropic.com/v1/organizations/usage_report/claude_code?starting_at=2025-09-08" \ --header "anthropic-version: 2023-06-01" \ --header "x-api-key: $ANTHROPIC_ADMIN_KEY" ``` For the available metrics, request parameters, and response schema, see the [Claude Code Analytics API guide](https://platform.claude.com/docs/en/manage-claude/claude-code-analytics-api) and the [API reference](https://platform.claude.com/docs/en/api/admin/usage_report/retrieve_claude_code). ## Get access to the Claude Enterprise Analytics API The Claude Enterprise Analytics API is available to Claude Enterprise organizations. Engagement and adoption data is available on all Enterprise plans. The cost and usage endpoints apply to usage-based Enterprise plans; for seat-based Enterprise plans, they reflect usage credits only. Only the primary owner of the organization can enable API access and create Analytics API keys. Go to [claude.ai > Organization settings > API](https://claude.ai/admin-settings/api-access) and enable public API access, then create an Analytics API key. Keys carry the `read:analytics` scope. Copy the displayed secret and store it in your secrets manager. Pass the key in the `x-api-key` header. Endpoints live under `https://api.anthropic.com/v1/organizations/analytics/`. For request examples, parameters, and response schemas, see the [Claude Enterprise Analytics API reference](https://platform.claude.com/docs/en/api/admin/analytics). The Claude Enterprise Analytics API provides: * **User activity:** per-user daily metrics across chat (conversations, messages, projects, files, artifacts), Claude Code (sessions, commits, pull requests, lines of code, tool actions), and other Claude products * **Activity summaries:** organization-level daily, weekly, and monthly active users, seat counts, and pending invites * **Project, skill, and connector usage:** adoption breakdowns for chat projects, skills, and connectors * **Cost and usage reports:** per-user and organization-level token usage and cost over time (usage-based Enterprise plans) For endpoint details, parameters, and response schemas, see the [Claude Enterprise Analytics API reference](https://platform.claude.com/docs/en/api/admin/analytics). The following sections cover data freshness, metric definitions, and operational guidance that apply across those endpoints. ## Data availability and freshness Claude Enterprise Analytics API data is available for dates on or after January 1, 2026. **Engagement and adoption endpoints** (user activity, summaries, projects, skills, connectors) return a per-day snapshot for the date you specify. Data for a given day is aggregated at 10UTC the following day and is typically available with a 1-day lag. Exact freshness varies by query, so rather than assuming a fixed lag, check the error response: requesting a date that is not yet available returns a 400 error naming the most recent available day. If data is not available well past the typical lag, it usually indicates a data pipeline failure on Anthropic's side; contact support if the gap persists. **Cost and usage endpoints** follow a different freshness model. Data is typically available within four hours of the underlying usage but may take up to 24 hours. Values for a given date can be revised for up to 30 days as late events arrive and reconciliation runs. For invoicing-grade totals, query dates at least 30 days in the past. Cost and usage responses include a `data_refreshed_at` timestamp. When `ending_at` is omitted (the default is the current time), the response includes a tail of data after `data_refreshed_at` that is incomplete. For stable results across repeated calls, set `ending_at` to a value at or before a previously returned `data_refreshed_at`. ## How metrics are defined **Active users.** A user counts as active for a day if any of the following is true: they sent at least one chat message in Claude, they had at least one Claude Code session (local or remote) associated with your Claude Enterprise organization that included tool use or git activity, or they had at least one Cowork session with tool use or message activity. **Per-product metric blocks.** Per-product metric objects (for example, Office Agent or Cowork metrics on a user-activity record) are always present on every record. Organizations without usage of that product see all-zero values rather than `null`. **Connector names.** Connector names are normalized across sources. For example, `Atlassian MCP server`, `mcp-atlassian`, and `atlassian_MCP` all appear as `atlassian` in the connector usage endpoint. ## Working with the API **Pagination cursors are bound to the query that issued them.** On the cost and usage endpoints, do not change query parameters mid-sequence: if you change `products[]`, `group_by[]`, `order_by`, the date range, or any filter and pass an old cursor, the request returns a 400 error. To change parameters, restart from the first page without a cursor. **List parameters use bracket notation.** Repeat the parameter for each value, for example `products[]=chat&products[]=claude_code`. **Amount fields are decimal strings in cents.** Currency amounts are returned as decimal strings such as `"41280.000000"` (which represents $412.80). To convert to dollars, parse as a decimal and divide by 100. Avoid binary floating-point parsing for values that may exceed several million dollars. **Rate limits apply at the organization level**, not per key, with a default of 60 requests per minute across all endpoints in this API. If that is not sufficient for your use case, contact your Anthropic account team to discuss adjusting the limit. ## Known limitations If your organization uses Claude Code through Amazon Bedrock, the Claude Enterprise Analytics API does not return Claude Code activity for that usage. ## Next steps Track Claude Code sessions, code changes, and tool usage with an Admin API key. Track API token usage and costs for your organization. Endpoint reference for engagement, adoption, and cost data. Audit and compliance data uses its own key types. --- title: Claude Code Analytics API url: https://platform.claude.com/docs/en/manage-claude/claude-code-analytics-api description: Programmatically access your organization's Claude Code usage analytics and productivity metrics with the Claude Code Analytics Admin API. --- **The Admin API is unavailable for individual accounts.** To collaborate with teammates and add members, set up your organization in **Console → Settings → Organization**. The Claude Code Analytics Admin API provides programmatic access to daily aggregated usage metrics for Claude Code users, enabling organizations to analyze developer productivity and build custom dashboards. This API provides more detail than the basic [Analytics dashboard](https://platform.claude.com/claude-code) without the complexity of the OpenTelemetry integration. This API enables you to better monitor, analyze, and optimize your Claude Code adoption: * **Developer productivity analysis:** Track sessions, lines of code added/removed, commits, and pull requests created using Claude Code * **Tool usage metrics:** Monitor acceptance and rejection rates for different Claude Code tools (Edit, MultiEdit, Write, NotebookEdit) * **Cost analysis:** View estimated costs and token usage broken down by Claude model * **Custom reporting:** Export data to build executive dashboards and reports for management teams * **Usage justification:** Provide metrics to justify and expand Claude Code adoption internally **Admin API key required.** These endpoints require an Admin API key, which is different from a standard Claude API key. See [Create an Admin API key](https://platform.claude.com/docs/en/manage-claude/admin-api-keys) to find where to create one for your organization type and which scopes to select. **Claude Platform on AWS:** The Claude Code Analytics API is not currently available. View Claude Code usage on the **Usage** page in the Claude Console instead. **Claude Enterprise organizations:** Claude Code activity for claude.ai users is reported by the Claude Enterprise Analytics API, which uses an Analytics API key instead of an Admin API key. See [Analytics APIs](https://platform.claude.com/docs/en/manage-claude/analytics-api) to find which API and key type your organization needs. ## Quick start Get your organization's Claude Code analytics for a specific day: ```bash cURL curl "https://api.anthropic.com/v1/organizations/usage_report/claude_code?\ starting_at=2025-09-08&\ limit=20" \ -H "anthropic-version: 2023-06-01" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" ``` **Set a User-Agent header for integrations** If you're building an integration, set your User-Agent header to help Anthropic understand usage patterns: ```text wrap User-Agent: YourApp/1.0.0 (https://yourapp.com) ``` ## Claude Code Analytics API Track Claude Code usage, productivity metrics, and developer activity across your organization with the `/v1/organizations/usage_report/claude_code` endpoint. ### Key concepts * **Daily aggregation:** Returns metrics for a single day specified by the `starting_at` parameter * **User-level data:** Each record represents one user's activity for the specified day * **Productivity metrics:** Track sessions, lines of code, commits, pull requests, and tool usage * **Token and cost data:** Monitor usage and estimated costs broken down by Claude model * **Cursor-based pagination:** Handle large datasets with stable pagination using opaque cursors * **Data freshness:** Metrics are available with up to 1-hour delay for consistency For complete parameter details and response schemas, see the [Claude Code Analytics API reference](https://platform.claude.com/docs/en/api/admin/usage_report/retrieve_claude_code). ### Basic examples #### Get analytics for a specific day ```bash cURL curl "https://api.anthropic.com/v1/organizations/usage_report/claude_code?\ starting_at=2025-09-08" \ -H "anthropic-version: 2023-06-01" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" ``` #### Get analytics with pagination ```bash cURL # First request curl "https://api.anthropic.com/v1/organizations/usage_report/claude_code?\ starting_at=2025-09-08&\ limit=20" \ -H "anthropic-version: 2023-06-01" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" # Subsequent request using cursor from response curl "https://api.anthropic.com/v1/organizations/usage_report/claude_code?\ starting_at=2025-09-08&\ page=page_MjAyNS0wNS0xNFQwMDowMDowMFo=" \ -H "anthropic-version: 2023-06-01" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" ``` ### Request parameters | Parameter | Type | Required | Description | | ------------- | ------- | -------- | ----------------------------------------------------------------------- | | `starting_at` | string | Yes | UTC date in YYYY-MM-DD format; returns metrics for this single day only | | `limit` | integer | No | Number of records per page (default: 20, max: 1000) | | `page` | string | No | Opaque cursor token from previous response's `next_page` field | ### Available metrics Each response record contains the following metrics for a single user on a single day: #### Dimensions * **date:** Date in RFC 3339 format (UTC timestamp) * **actor:** The user or API key that performed the Claude Code actions (either `user_actor` with `email_address` or `api_actor` with `api_key_name`) * **organization\_id:** Organization UUID * **customer\_type:** Type of customer account (`api` for API customers, `subscription` for Pro/Team customers) * **terminal\_type:** Type of terminal or environment where Claude Code was used (for example, `vscode`, `iTerm.app`, `tmux`) #### Core metrics * **num\_sessions:** Number of distinct Claude Code sessions initiated by this actor * **lines\_of\_code.added:** Total number of lines of code added across all files by Claude Code * **lines\_of\_code.removed:** Total number of lines of code removed across all files by Claude Code * **commits\_by\_claude\_code:** Number of git commits created through Claude Code's commit functionality * **pull\_requests\_by\_claude\_code:** Number of pull requests created through Claude Code's PR functionality #### Tool action metrics Breakdown of tool action acceptance and rejection rates by tool type: * **edit\_tool.accepted/rejected:** Number of Edit tool proposals that the user accepted/rejected * **multi\_edit\_tool.accepted/rejected:** Number of MultiEdit tool proposals that the user accepted/rejected * **write\_tool.accepted/rejected:** Number of Write tool proposals that the user accepted/rejected * **notebook\_edit\_tool.accepted/rejected:** Number of NotebookEdit tool proposals that the user accepted/rejected #### Model breakdown For each Claude model used: * **model:** Claude model identifier (for example, `claude-opus-5`) * **tokens.input/output:** Input and output token counts for this model * **tokens.cache\_read/cache\_creation:** Cache-related token usage for this model * **estimated\_cost.amount:** Estimated cost in cents USD for this model * **estimated\_cost.currency:** Currency code for the cost amount (currently always `USD`) ### Response structure The API returns data in the following format: ```json { "data": [ { "date": "2025-09-08T00:00:00Z", "actor": { "type": "user_actor", "email_address": "developer@company.com" }, "organization_id": "dc9f6c26-b22c-4831-8d01-0446bada88f1", "customer_type": "api", "terminal_type": "vscode", "core_metrics": { "num_sessions": 5, "lines_of_code": { "added": 1543, "removed": 892 }, "commits_by_claude_code": 12, "pull_requests_by_claude_code": 2 }, "tool_actions": { "edit_tool": { "accepted": 45, "rejected": 5 }, "multi_edit_tool": { "accepted": 12, "rejected": 2 }, "write_tool": { "accepted": 8, "rejected": 1 }, "notebook_edit_tool": { "accepted": 3, "rejected": 0 } }, "model_breakdown": [ { "model": "claude-opus-5", "tokens": { "input": 100000, "output": 35000, "cache_read": 10000, "cache_creation": 5000 }, "estimated_cost": { "currency": "USD", "amount": 141 } } ] } ], "has_more": false, "next_page": null } ``` ## Pagination The API supports cursor-based pagination for organizations with large numbers of users: 1. Make your initial request with optional `limit` parameter. 2. If `has_more` is `true` in the response, use the `next_page` value in your next request. 3. Continue until `has_more` is `false`. The cursor encodes the position of the last record and ensures stable pagination even as new data arrives. Each pagination session maintains a consistent data boundary to ensure you don't miss or duplicate records. ## Common use cases * **Executive dashboards:** Create high-level reports showing Claude Code impact on development velocity * **AI tool comparison:** Export metrics to compare Claude Code with other AI coding tools such as Copilot and Cursor * **Developer productivity analysis:** Track individual and team productivity metrics over time * **Cost tracking and allocation:** Monitor spending patterns and allocate costs by team or project * **Adoption monitoring:** Identify which teams and users are getting the most value from Claude Code * **ROI justification:** Provide concrete metrics to justify and expand Claude Code adoption internally ## Frequently asked questions ### How fresh is the analytics data? Claude Code analytics data typically appears within 1 hour of user activity completion. To ensure consistent pagination results, only data older than 1 hour is included in responses. ### Can I get real-time metrics? No, this API provides daily aggregated metrics only. For real-time monitoring, consider using the [OpenTelemetry integration](https://code.claude.com/docs/en/monitoring-usage). ### How are users identified in the data? Users are identified through the `actor` field in two ways: * **`user_actor`:** Contains `email_address` for users who authenticate through OAuth (most common) * **`api_actor`:** Contains `api_key_name` for users who authenticate with an API key The `customer_type` field indicates whether the usage is from `api` customers (pay-as-you-go API) or `subscription` customers (Pro/Team plans). ### What's the data retention period? Historical Claude Code analytics data is retained and accessible through the API. There is no specified deletion period for this data. ### Which Claude Code deployments are supported? This API only tracks Claude Code usage on the Claude API. Usage through [Claude in Amazon Bedrock](https://platform.claude.com/docs/en/build-with-claude/claude-in-amazon-bedrock), [Claude in Microsoft Foundry](https://platform.claude.com/docs/en/build-with-claude/claude-in-microsoft-foundry), [Claude on Google Cloud](https://platform.claude.com/docs/en/build-with-claude/claude-on-vertex-ai), or [Claude Platform on AWS](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws) is not included. ### What does it cost to use this API? The Claude Code Analytics API is free to use for all organizations with access to the Admin API. ### How do I calculate tool acceptance rates? Tool acceptance rate = `accepted / (accepted + rejected)` for each tool type. For example, if the edit tool shows 45 accepted and 5 rejected, the acceptance rate is 90%. ### What time zone is used for the date parameter? All dates are in UTC. The `starting_at` parameter should be in YYYY-MM-DD format and represents UTC midnight for that day. ## See also The Claude Code Analytics API helps you understand and optimize your team's development workflow. Learn more about related features: * [Admin API](https://platform.claude.com/docs/en/manage-claude/admin-api) * [Admin API reference](https://platform.claude.com/docs/en/api/admin) * [Claude Code Analytics dashboard](https://platform.claude.com/claude-code) * [Usage and Cost API](https://platform.claude.com/docs/en/manage-claude/usage-cost-api) - Track API usage across all Anthropic services * [Compliance API](https://platform.claude.com/docs/en/manage-claude/compliance-api) - Retrieve audit and activity data * [Identity and access management](https://code.claude.com/docs/en/iam) * [Monitoring usage with OpenTelemetry](https://code.claude.com/docs/en/monitoring-usage) for custom metrics and alerting --- title: Rate Limits API url: https://platform.claude.com/docs/en/manage-claude/rate-limits-api description: Programmatically query your organization's API rate limits with the Rate Limits API. --- **The Admin API is unavailable for individual accounts.** To collaborate with teammates and add members, set up your organization in **Console → Settings → Organization**. The Rate Limits API provides programmatic access to the rate limits configured for your organization and its workspaces. This is the same information shown on the [Rate limits](https://platform.claude.com/settings/limits) page in the Claude Console. Use this API to: * **Keep gateways and proxies in sync:** Read your current limits at startup and on a schedule instead of hardcoding values that drift when Anthropic adjusts them. * **Power internal alerting:** Compare usage data from the [Usage and Cost API](https://platform.claude.com/docs/en/manage-claude/usage-cost-api) against your configured limits. * **Audit workspace configuration:** Verify that workspace overrides match what your provisioning automation expects. **Admin API key required.** These endpoints require an Admin API key, which is different from a standard Claude API key. See [Create an Admin API key](https://platform.claude.com/docs/en/manage-claude/admin-api-keys) to find where to create one for your organization type and which scopes to select. ## Quick start List the rate limits configured for your organization: ```bash cURL curl "https://api.anthropic.com/v1/organizations/rate_limits" \ --header "anthropic-version: 2023-06-01" \ --header "x-api-key: $ANTHROPIC_ADMIN_KEY" ``` ## Organization rate limits The `/v1/organizations/rate_limits` endpoint returns the rate limits applied at the organization level for the Messages API and its supporting resources. Limits for other products, such as [Claude Managed Agents](https://platform.claude.com/docs/en/managed-agents/overview), are not included. ### Key concepts * **Rate limit groups:** Each entry in the response represents one rate limit group. Model rate limits are grouped so that several model versions share a single set of limits, and other groups cover resources such as the Message Batches API, the Files API, the Token Counting API, agent skills, and the web search tool. * **`group_type`:** Identifies which category of limits the entry covers. See [Filtering by group type](https://platform.claude.com/docs/en/manage-claude/rate-limits-api#filtering-by-group-type) for the list of values. * **`models` list:** For `model_group` entries, the `models` field lists every model ID and alias that counts against that group's limits. Use this list to look up which group any model string falls under. For other group types, `models` is `null`. * **`limits` list:** Each group carries a list of `{type, value}` pairs. The `type` field identifies the limiter (such as `requests_per_minute`, `input_tokens_per_minute`, or `output_tokens_per_minute`) and `value` is the configured limit. See [Rate limits](https://platform.claude.com/docs/en/api/rate-limits) for how each limiter is measured and enforced. For complete parameter details and response schemas, see the [Organization Rate Limits API reference](https://platform.claude.com/docs/en/api/admin/rate_limits/list). ### List all organization rate limits ```bash cURL curl "https://api.anthropic.com/v1/organizations/rate_limits" \ --header "anthropic-version: 2023-06-01" \ --header "x-api-key: $ANTHROPIC_ADMIN_KEY" ``` ```json { "data": [ { "type": "rate_limit", "group_type": "model_group", "models": ["claude-opus-5"], "limits": [ { "type": "requests_per_minute", "value": 4000 }, { "type": "input_tokens_per_minute", "value": 10000000 }, { "type": "output_tokens_per_minute", "value": 800000 } ] }, { "type": "rate_limit", "group_type": "model_group", "models": [ "claude-opus-4-5", "claude-opus-4-5-20251101", "claude-opus-4-6", "claude-opus-4-7", "claude-opus-4-8" ], "limits": [ { "type": "requests_per_minute", "value": 4000 }, { "type": "input_tokens_per_minute", "value": 10000000 }, { "type": "output_tokens_per_minute", "value": 800000 } ] }, { "type": "rate_limit", "group_type": "batch", "models": null, "limits": [{ "type": "enqueued_batch_requests", "value": 500000 }] } ], "next_page": null } ``` ### Look up the limits for a specific model Pass any model ID or alias as the `model` query parameter to return only the entry that contains it: ```bash cURL curl "https://api.anthropic.com/v1/organizations/rate_limits?model=claude-opus-5" \ --header "anthropic-version: 2023-06-01" \ --header "x-api-key: $ANTHROPIC_ADMIN_KEY" ``` If the model string doesn't match any group, the endpoint returns a 404 error. The `model` parameter is supported on the organization endpoint only; the workspace endpoint doesn't accept it. ## Workspace rate limits The `/v1/organizations/workspaces/{workspace_id}/rate_limits` endpoint returns the rate limit overrides configured for a single workspace. The response only includes overrides, so anything missing from it is inherited from the organization: * A group that is absent from `data` has no workspace override at all. The workspace inherits the organization-level limits for that group (it is not unlimited). * Within a group that is present, a limiter type that is absent from `limits[]` has no workspace override for that limiter. The workspace inherits the organization value for it. * For each limiter that is present, `org_limit` is the organization-level value for the same limiter, or `null` if the organization has no configured limit for that limiter type. For complete parameter details and response schemas, see the [Workspace Rate Limits API reference](https://platform.claude.com/docs/en/api/admin/workspaces/rate_limits/list). To retrieve your organization's workspace IDs, use the [List Workspaces](https://platform.claude.com/docs/en/api/admin/workspaces/list) endpoint, or find them in the [Claude Console](https://platform.claude.com/settings/workspaces). The default workspace cannot have rate limit overrides, so it has no entry on this endpoint; use the organization endpoint to read its limits. ```bash cURL curl "https://api.anthropic.com/v1/organizations/workspaces/wrkspc_01JwQvzr7rXLA5AGx3HKfFUJ/rate_limits" \ --header "anthropic-version: 2023-06-01" \ --header "x-api-key: $ANTHROPIC_ADMIN_KEY" ``` ```json { "data": [ { "type": "workspace_rate_limit", "group_type": "model_group", "models": ["claude-opus-5"], "limits": [ { "type": "requests_per_minute", "value": 1000, "org_limit": 4000 }, { "type": "input_tokens_per_minute", "value": 500000, "org_limit": 10000000 } ] }, { "type": "workspace_rate_limit", "group_type": "model_group", "models": [ "claude-opus-4-5", "claude-opus-4-5-20251101", "claude-opus-4-6", "claude-opus-4-7", "claude-opus-4-8" ], "limits": [ { "type": "requests_per_minute", "value": 1000, "org_limit": 4000 }, { "type": "input_tokens_per_minute", "value": 500000, "org_limit": 10000000 } ] } ], "next_page": null } ``` ## Filtering by group type Both endpoints accept an optional `group_type` query parameter that restricts the response to a single category: ```bash cURL curl "https://api.anthropic.com/v1/organizations/rate_limits?group_type=batch" \ --header "anthropic-version: 2023-06-01" \ --header "x-api-key: $ANTHROPIC_ADMIN_KEY" ``` Valid values are `model_group`, `batch`, `token_count`, `files`, `skills`, and `web_search`. ## Pagination Both endpoints accept a `page` query parameter and return a `next_page` field. Responses are currently always a single page, so `next_page` is `null`. Loop on `next_page` so your client paginates correctly without changes when the response grows. ## Frequently asked questions ### Which model strings appear in the `models` list? Every model ID and alias that counts against the group, including dated IDs (such as `claude-sonnet-4-5-20250929`) and undated aliases (such as `claude-sonnet-4-5`). Look up any model string you pass to the Messages API and you'll find it in exactly one `model_group` entry. ### What does it mean if a group is missing from the workspace response? The workspace has no override for that group and inherits the organization-level limit. Query the organization endpoint to see the inherited values. ### Can I update rate limits with this API? No. To set workspace rate limits, open the workspace in the [Claude Console](https://platform.claude.com/settings/workspaces) and use the **Rate limits** tab. ## See also * [Rate limits](https://platform.claude.com/docs/en/api/rate-limits) * [Admin API](https://platform.claude.com/docs/en/manage-claude/admin-api) * [Admin API reference](https://platform.claude.com/docs/en/api/admin) * [Workspaces](https://platform.claude.com/docs/en/manage-claude/workspaces) * [Usage and Cost API](https://platform.claude.com/docs/en/manage-claude/usage-cost-api) --- title: Spend Limits API url: https://platform.claude.com/docs/en/manage-claude/spend-limits-api description: Set a spend limit on each Claude Enterprise member, see where each member's spend limit is inherited from, and review or act on members' requests for a higher limit. --- The Spend Limits API lets you set a spend limit on each Claude Enterprise member, see where each member's spend limit is inherited from, and review or act on members' requests for a higher limit. For per-user and time-bucketed usage and cost *reporting*, see [Analytics APIs](https://platform.claude.com/docs/en/manage-claude/analytics-api). **Scoped Admin API key required** These endpoints require an Admin API key with the `read:spend_limits` scope (for `GET` endpoints) or the `write:spend_limits` scope (for `POST` and `DELETE` endpoints). See [Create an Admin API key](https://platform.claude.com/docs/en/manage-claude/admin-api-keys#create-a-key-for-a-claude-enterprise-organization) for where your primary owner creates one and which scopes to select. Pass the key in the `x-api-key` header on every request. The Spend Limits API is available to Claude Enterprise organizations only. It is not available to Claude Platform (Claude Console) organizations. ## Overview The API exposes eight endpoints across two resources: | Resource | Endpoints | Use for | | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | **Spend limits** | `GET /v1/organizations/spend_limits/effective` `GET /v1/organizations/spend_limits/{spend_limit_id}` `POST /v1/organizations/spend_limits` `DELETE /v1/organizations/spend_limits/{spend_limit_id}` | Read each member's effective spend limit and period-to-date spend; set or clear a per-user override. | | **Spend limit increase requests** | `GET /v1/organizations/spend_limit_increase_requests` `GET /v1/organizations/spend_limit_increase_requests/{id}` `POST /v1/organizations/spend_limit_increase_requests/{id}/approve` `POST /v1/organizations/spend_limit_increase_requests/{id}/deny` | List members' requests for a higher spend limit, with the context needed to decide; approve or deny each request. | Use the **spend limits** endpoints to answer "what spend limit applies to each member, where does it come from, and how close are they to it?" and to set a per-user override. Use the **spend limit increase requests** endpoints to work the queue of member-submitted requests. ## Prerequisites * Your organization must be on a Claude Enterprise plan. * Usage credits must be turned on for your organization. Your primary owner can turn them on in claude.ai billing settings. ## Quick start List every member's effective monthly spend limit and period-to-date spend: ```bash cURL curl "https://api.anthropic.com/v1/organizations/spend_limits/effective?limit=20" \ --header "x-api-key: $ANTHROPIC_ADMIN_KEY" ``` ## Key concepts ### The spend limit hierarchy An **effective spend limit** applies to each member's spend, resolved from a hierarchy of scope levels. When a member has no per-user override, they inherit the spend limit configured for their group (if your organization uses group-based limits), their seat tier, or the organization-wide default. A group spend limit is a per-member default: each member inheriting it is gated against their own spend, not a pooled group budget. Reading `GET /v1/organizations/spend_limits/effective` returns every current member with their resolved effective spend limit, where that limit was resolved from (`source`), and their period-to-date spend. Setting a per-user override with `POST /v1/organizations/spend_limits` pins a member to a specific spend limit regardless of what they would otherwise inherit. Deleting the override returns them to the inherited spend limit (or leaves them unlimited if none exists). The `source` field on each member's row tells you which level their spend limit resolved from: `user` (a per-user override), `seat_tier`, `rbac_group`, or `organization`. Treat scope types as an open set; fall through on unknown values rather than failing. ### Period `period` is the recurring window over which the spend limit is enforced and spend resets. A spend limit is identified by its `(scope, period)` pair. Currently `monthly` is the only supported period; monthly spend resets at 00UTC on the first of each calendar month. Treat `period` as an open set. ### Amounts and currency All monetary values are strings in **minor units of the organization's billing currency** (cents, for USD). For example, `"50000"` represents 500.00 USD. Parse as a decimal and divide by 100 to display dollars; avoid binary floating-point for large values. `amount` is **nullable**. In a member's effective row, `null` means **unlimited** (no spend limit) and `"0"` means the member cannot use Claude beyond their plan's included usage. On a configured spend-limit row (as returned by `GET /v1/organizations/spend_limits/{id}`), `null` only means no numeric spend limit is set; read the member's effective row to distinguish unlimited from included-usage-only. `period_to_date_spend` is the member's spend accrued since the start of the current `period`, in the same minor-unit format; it may include a fractional part (for example, `"41280.125"`). It may read as `"0"` if the spend reading is temporarily unavailable; treat it as informational, not transactional. ### Increase request lifecycle A **spend limit increase request** is created when a member clicks **Request more usage** in claude.ai. Requests are not created through this API. A request's `status` is one of: | Status | Meaning | | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `pending` | Awaiting admin action. The request normally carries a live `spend_summary` so you can see the member's current effective spend limit and period-to-date spend while deciding; `spend_summary` may be `null` if it could not be computed. | | `approved` | The request was resolved with approval: either an admin approved it explicitly, another admin action raised the member's spend limit, or Anthropic support raised a spend limit on the organization's behalf. `spend_summary` is `null`. | | `denied` | An admin declined. `spend_summary` is `null`. claude.ai hides that member's request button for 30 days from `resolved_at`; an admin can still raise the member's spend limit directly at any time. | Both `approved` and `denied` are terminal. A member has at most one `pending` request at a time. Approving with `POST /v1/organizations/spend_limit_increase_requests/{id}/approve` writes the same per-user spend limit row that `POST /v1/organizations/spend_limits` writes. Setting a spend limit directly does **not** transition a pending request; use the approve endpoint to resolve a request. By default, Anthropic emails the member when their request is approved or denied. Pass `suppress_notification: true` on approve or deny to suppress that email (for example, when your own system notifies the member). ## Rate limiting All eight endpoints share a single per-organization limit of **60 requests per minute**. Requests over the limit return **429 Too Many Requests**. ## Pagination `GET /v1/organizations/spend_limits/effective` and `GET /v1/organizations/spend_limit_increase_requests` are paginated with an **opaque cursor**. The first request returns up to `limit` rows plus a `next_page` cursor; pass that cursor unchanged as the `page` parameter on the next request, and repeat until `next_page` is `null`. **Do not change query parameters mid-sequence.** Cursors are bound to the filters that issued them. If you change `user_ids[]`, `period[]`, `status[]`, or `actor_ids[]` and pass an old cursor, you'll get a 400 with *"cursor does not match current query parameters"*. Start a new sequence from the first page instead. ## Serializing list parameters List parameters use bracket notation: repeat the parameter name with `[]` for each value. ```text wrap user_ids[]=user_01AbCdEfGh&user_ids[]=user_01JkLmNoPq ``` ## Error responses Error responses follow the standard shape documented in [Errors](https://platform.claude.com/docs/en/api/errors). Quote the `request_id` from the response body when contacting support. ## Spend limits ### List each member's effective spend limit `GET /v1/organizations/spend_limits/effective` returns one row per current member, reflecting each member's effective spend limit, its `source` in the scope hierarchy, and their `period_to_date_spend`. Requires the `read:spend_limits` scope. For complete parameter details and response schemas, see [List effective spend limits](https://platform.claude.com/docs/en/api/admin/spend_limits/list_effective) in the API reference. ```bash cURL curl "https://api.anthropic.com/v1/organizations/spend_limits/effective?limit=20" \ --header "x-api-key: $ANTHROPIC_ADMIN_KEY" ``` ```json { "data": [ { "scope": { "type": "user", "user_id": "user_01AbCdEfGh" }, "actor": { "type": "user_actor", "user_id": "user_01AbCdEfGh", "name": "Jane Smith", "email_address": "jane@example.com", "deleted": false }, "amount": "50000", "currency": "USD", "period": "monthly", "source": { "type": "seat_tier", "seat_tier": "enterprise_standard" }, "spend_limit_id": "spl_01XyZaBcDeFgHiJkLmNoPq", "period_to_date_spend": "31402.5" } ], "next_page": "page_..." } ``` ### Get a single spend limit `GET /v1/organizations/spend_limits/{spend_limit_id}` returns one configured spend limit by ID. Use it to inspect the row that a `spend_limit_id` field referenced. Requires the `read:spend_limits` scope. For complete parameter details and response schemas, see [Retrieve a spend limit](https://platform.claude.com/docs/en/api/admin/spend_limits/retrieve) in the API reference. ```bash cURL curl "https://api.anthropic.com/v1/organizations/spend_limits/spl_01AbCdEfGhIjKlMnOpQrSt" \ --header "x-api-key: $ANTHROPIC_ADMIN_KEY" ``` ### Set a per-user override `POST /v1/organizations/spend_limits` sets a per-user spend limit override. This is an upsert keyed on `(scope, period)`: setting a limit for a user and period that already has one overwrites it in place. This endpoint accepts only `scope.type: "user"`; seat-tier, group, and organization-level defaults are configured in claude.ai settings. Requires the `write:spend_limits` scope. For complete parameter details and response schemas, see [Create a spend limit](https://platform.claude.com/docs/en/api/admin/spend_limits/create) in the API reference. ```bash cURL curl --request POST "https://api.anthropic.com/v1/organizations/spend_limits" \ --header "content-type: application/json" \ --header "x-api-key: $ANTHROPIC_ADMIN_KEY" \ --data '{"scope": {"type": "user", "user_id": "user_01AbCdEfGh"}, "amount": "75000"}' ``` ```json { "type": "spend_limit", "id": "spl_01RsTuVwXyZaBcDeFgHiJk", "created_at": "2026-05-11T10:02:44Z", "updated_at": "2026-05-11T10:02:44Z", "scope": { "type": "user", "user_id": "user_01AbCdEfGh" }, "amount": "75000", "currency": "USD", "period": "monthly" } ``` ### Remove a per-user override `DELETE /v1/organizations/spend_limits/{spend_limit_id}` removes a per-user override, after which the member falls back to any inherited seat-tier, group, or organization default. Seat-tier, group, and organization-level rows cannot be deleted through this endpoint. Requires the `write:spend_limits` scope. For complete parameter details and response schemas, see [Delete a spend limit](https://platform.claude.com/docs/en/api/admin/spend_limits/delete) in the API reference. ```bash cURL curl --request DELETE "https://api.anthropic.com/v1/organizations/spend_limits/spl_01RsTuVwXyZaBcDeFgHiJk" \ --header "x-api-key: $ANTHROPIC_ADMIN_KEY" ``` ## Spend limit increase requests ### List increase requests `GET /v1/organizations/spend_limit_increase_requests` lists requests, most recent first. Filter by `status[]` (`pending`, `approved`, `denied`) and `actor_ids[]`. The list excludes requests whose requester is no longer a member of the organization. Requires the `read:spend_limits` scope. For complete parameter details and response schemas, see [List spend limit increase requests](https://platform.claude.com/docs/en/api/admin/spend_limits/increase_requests/list) in the API reference. ```bash cURL curl "https://api.anthropic.com/v1/organizations/spend_limit_increase_requests?status[]=pending&limit=50" \ --header "x-api-key: $ANTHROPIC_ADMIN_KEY" ``` Each pending request carries a live `spend_summary` showing the requester's current effective spend limit and period-to-date spend, enough to decide without a separate lookup. ### Get a single increase request `GET /v1/organizations/spend_limit_increase_requests/{id}` returns one request by ID. Requires the `read:spend_limits` scope. For complete parameter details and response schemas, see [Retrieve a spend limit increase request](https://platform.claude.com/docs/en/api/admin/spend_limits/increase_requests/retrieve) in the API reference. ```bash cURL curl "https://api.anthropic.com/v1/organizations/spend_limit_increase_requests/slir_01AbCdEfGhIjKlMnOpQrSt" \ --header "x-api-key: $ANTHROPIC_ADMIN_KEY" ``` ### Approve an increase request `POST /v1/organizations/spend_limit_increase_requests/{id}/approve` approves a pending request: it writes a per-user spend limit at the admin-supplied `amount` for the requester and transitions the request to `approved`. The request does not carry a requested amount; you supply the new spend limit on approval. Requires the `write:spend_limits` scope. For complete parameter details and response schemas, see [Approve a spend limit increase request](https://platform.claude.com/docs/en/api/admin/spend_limits/increase_requests/approve) in the API reference. ```bash cURL curl --request POST "https://api.anthropic.com/v1/organizations/spend_limit_increase_requests/slir_01AbCdEfGhIjKlMnOpQrSt/approve" \ --header "content-type: application/json" \ --header "x-api-key: $ANTHROPIC_ADMIN_KEY" \ --data '{"amount": "75000", "suppress_notification": true}' ``` ### Deny an increase request `POST /v1/organizations/spend_limit_increase_requests/{id}/deny` denies a pending request. Idempotent on `denied`: denying an already-denied request returns 200 with the existing resource. The endpoint rejects an attempt to deny an already-approved request so automation can distinguish a retry from a conflicting decision. Requires the `write:spend_limits` scope. For complete parameter details and response schemas, see [Deny a spend limit increase request](https://platform.claude.com/docs/en/api/admin/spend_limits/increase_requests/deny) in the API reference. ```bash cURL curl --request POST "https://api.anthropic.com/v1/organizations/spend_limit_increase_requests/slir_01AbCdEfGhIjKlMnOpQrSt/deny" \ --header "content-type: application/json" \ --header "x-api-key: $ANTHROPIC_ADMIN_KEY" \ --data '{"suppress_notification": true}' ``` ## Example workflows These workflows combine the Spend Limits API with the [Analytics APIs](https://platform.claude.com/docs/en/manage-claude/analytics-api) cost endpoints. The Analytics cost endpoints are designed for organization-wide spend reporting across a date range. `GET /spend_limits/effective` returns the cap that currently applies to each member. Start a sweep with Analytics to discover which members to look at, then read their current caps with `/effective`. Spend Limits endpoints require the `spend_limits` scopes and Analytics cost endpoints require `read:analytics`; see [Analytics APIs](https://platform.claude.com/docs/en/manage-claude/analytics-api) for how to provision access. All monetary values on both are decimal strings in minor units (cents). Both APIs paginate with an opaque cursor. Set an explicit `limit` and page through `next_page` until it's `null` to cover the whole organization. ### Automate the increase-request review flow Run a scheduled job that fetches pending requests, applies your organization's approval policy, and resolves each one. 1. List pending requests: ```bash cURL curl "https://api.anthropic.com/v1/organizations/spend_limit_increase_requests?status[]=pending&limit=100" \ --header "x-api-key: $ANTHROPIC_ADMIN_KEY" ``` Each request carries the requester's `actor.user_id` and a live `spend_summary` with their current effective `amount` and `period_to_date_spend`, enough to decide without a separate lookup. 2. Apply your policy. For example, auto-approve when the member's current `amount` is below a threshold, and route larger caps for manual review. 3. Resolve each request. To approve, supply the new cap: ```bash cURL curl --request POST "https://api.anthropic.com/v1/organizations/spend_limit_increase_requests/{id}/approve" \ --header "content-type: application/json" \ --header "x-api-key: $ANTHROPIC_ADMIN_KEY" \ --data '{"amount": "75000", "suppress_notification": true}' ``` To deny, `POST` to `.../{id}/deny` instead. Pass `suppress_notification: true` when your own system notifies the requester. ### Identify members close to their spend limit Find members approaching their cap so you can raise it before they're blocked. 1. Pull each member's month-to-date spend from the Analytics API (one row per member, highest spend first by default): ```bash cURL curl "https://api.anthropic.com/v1/organizations/analytics/user_cost_report?starting_at=2026-06-01T00:00:00Z&limit=1000" \ --header "x-api-key: $ANALYTICS_API_KEY" ``` Each row carries `actor.user_id`, `actor.email`, and `amount` (the member's spend in cents). Page through `next_page` to cover the whole organization. 2. For the top spenders (or everyone above a dollar threshold), fetch effective caps in batches: ```bash cURL curl "https://api.anthropic.com/v1/organizations/spend_limits/effective?user_ids[]=user_01Ab...&user_ids[]=user_01Cd...&limit=100" \ --header "x-api-key: $ANTHROPIC_ADMIN_KEY" ``` Each row returns the cap as `amount` (`null` = unlimited, `"0"` = included usage only) alongside `period_to_date_spend`. 3. For each member with a positive cap, compute `period_to_date_spend / amount` and flag those at or above your threshold (for example, 80 percent). Treat a `"0"` cap as already at-limit. There is no server-side filter for this ratio. 4. Act on flagged members: raise the cap with `POST /v1/organizations/spend_limits`, approve a pending increase request if one exists, or reach out to the member. ### Find members with rapidly changing usage Surface members whose spend has jumped week over week. 1. Pull per-member daily cost for the trailing two weeks from the Analytics API: ```bash cURL curl "https://api.anthropic.com/v1/organizations/analytics/user_cost_report?starting_at=2026-06-09T00:00:00Z&ending_at=2026-06-23T00:00:00Z&bucket_width=1d&limit=1000" \ --header "x-api-key: $ANALYTICS_API_KEY" ``` With `bucket_width` set, each member spans one row per day with usage; page through `next_page` to collect every member's full series. 2. Group rows by `actor.user_id`. For each member, sum the most recent seven days and the prior seven days. Flag members whose recent week exceeds the prior week by your chosen multiple (for example, three). Recent-day cost is provisional and can be revised upward; for repeatable comparisons, set `ending_at` at or before a previously returned `data_refreshed_at` (see [Data availability and freshness](https://platform.claude.com/docs/en/manage-claude/analytics-api#data-availability-and-freshness)). 3. Act on flagged members: adjust the cap with `POST /v1/organizations/spend_limits`, or reach out. ## Frequently asked questions ### Does setting a spend limit directly resolve a member's pending increase request? No. `POST /v1/organizations/spend_limits` writes the override but leaves the pending request untouched. Use `POST /v1/organizations/spend_limit_increase_requests/{id}/approve` to resolve the request and write the override in one call. ### What happens when I delete a per-user override? The member falls back to whatever they'd inherit from the hierarchy: their group, seat-tier, or organization default. If no default exists at any level, the member is unlimited. ### Can I set a seat-tier or organization-wide default through this API? No. Only per-user overrides can be written through this API. Seat-tier, group, and organization-level defaults are configured in claude.ai Organization settings. ### Why does `period_to_date_spend` sometimes read as `"0"` for an active member? The spend reading can be temporarily unavailable, in which case the field reads `"0"` rather than erroring. Treat it as informational. ## See also Generated request and response schemas for every Spend Limits API endpoint. Generated request and response schemas for the increase-request endpoints. Per-user and time-bucketed usage and cost reporting for Claude Enterprise. --- title: Usage and Cost API url: https://platform.claude.com/docs/en/manage-claude/usage-cost-api description: Programmatically access your organization's API usage and cost data with the Usage & Cost Admin API. --- **The Admin API is unavailable for individual accounts.** To collaborate with teammates and add members, set up your organization in **Console → Settings → Organization**. The Usage & Cost Admin API provides programmatic and granular access to historical API usage and cost data for your organization. This data is similar to the information available in the [Usage](https://platform.claude.com/usage) and [Cost](https://platform.claude.com/cost) pages of the Claude Console. This API enables you to better monitor, analyze, and optimize your Claude implementations: * **Accurate usage tracking:** Get precise token counts and usage patterns instead of relying solely on response token counting * **Cost reconciliation:** Match internal records with Anthropic billing for finance and accounting teams * **Product performance and improvement:** Monitor product performance while measuring if changes to the system have improved it, or set up alerting * **[Rate limit](https://platform.claude.com/docs/en/api/rate-limits) optimization:** Optimize features like [prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) or specific prompts to make the most of your allocated capacity. * **Advanced analysis:** Perform deeper data analysis than what's available in Console **Admin API key required.** These endpoints require an Admin API key, which is different from a standard Claude API key. See [Create an Admin API key](https://platform.claude.com/docs/en/manage-claude/admin-api-keys) to find where to create one for your organization type and which scopes to select. Claude Enterprise organizations use an Analytics API key with a different API instead; see [Which API do you need?](https://platform.claude.com/docs/en/manage-claude/usage-cost-api#which-api-do-you-need). **Claude Platform on AWS:** The programmatic Usage and Cost API endpoints are not currently available. View usage and cost data on the **Usage** and **Cost** pages in the Claude Console instead. ## Which API do you need? Anthropic provides cost and usage reporting through two APIs, depending on which Claude product your organization manages: | Your organization | API | Key type | | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | | Claude Console (Claude Platform) | The Usage and Cost Admin API described on this page | Admin API key (`sk-ant-admin01-...`) | | Claude Enterprise (claude.ai) | The [Claude Enterprise Analytics API](https://platform.claude.com/docs/en/api/admin/analytics) cost and usage endpoints | Analytics API key | Claude Enterprise parent organizations do not appear in Claude Console and carry no Admin API keys, so for them the Analytics API key is the only path to this data. See [Analytics APIs](https://platform.claude.com/docs/en/manage-claude/analytics-api) for how to create each key type and which plans the Claude Enterprise cost data applies to. ## Partner solutions Leading observability platforms offer ready-to-use integrations for monitoring your Claude API usage and cost, without writing custom code. These integrations provide dashboards, alerting, and analytics to help you manage your API usage effectively. Cloud intelligence platform for tracking and forecasting costs LLM Observability with automatic tracing and monitoring Agentless integration for easy LLM observability with out-of-the-box dashboards and alerts Advanced querying and visualization through OpenTelemetry FinOps platform for LLM cost & usage observability ## Quick start Get your organization's daily usage for the last 7 days: ```bash cURL curl "https://api.anthropic.com/v1/organizations/usage_report/messages?\ starting_at=2025-01-08T00:00:00Z&\ ending_at=2025-01-15T00:00:00Z&\ bucket_width=1d" \ -H "anthropic-version: 2023-06-01" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" ``` **Set a User-Agent header for integrations** If you're building an integration, set your User-Agent header to help Anthropic understand usage patterns: ```text wrap User-Agent: YourApp/1.0.0 (https://yourapp.com) ``` ## Usage API Track token consumption across your organization with detailed breakdowns by model, workspace, and service tier with the `/v1/organizations/usage_report/messages` endpoint. ### Key concepts * **Time buckets:** Aggregate usage data in fixed intervals (`1m`, `1h`, or `1d`) * **Token tracking:** Measure uncached input, cached input, cache creation, and output tokens * **Filtering & grouping:** Filter by API key, workspace, model, service tier, context window, [data residency](https://platform.claude.com/docs/en/manage-claude/data-residency), or speed (beta), and group results by these dimensions * **Server tool usage:** Track usage of server-side tools such as web search For complete parameter details and response schemas, see the [Usage API reference](https://platform.claude.com/docs/en/api/admin-api/usage-cost/get-messages-usage-report). ### Basic examples #### Daily usage by model ```bash cURL curl "https://api.anthropic.com/v1/organizations/usage_report/messages?\ starting_at=2025-01-01T00:00:00Z&\ ending_at=2025-01-08T00:00:00Z&\ group_by[]=model&\ bucket_width=1d" \ -H "anthropic-version: 2023-06-01" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" ``` #### Hourly usage with filtering ```bash cURL curl "https://api.anthropic.com/v1/organizations/usage_report/messages?\ starting_at=2025-01-15T00:00:00Z&\ ending_at=2025-01-15T23:59:59Z&\ models[]=claude-opus-5&\ service_tiers[]=batch&\ context_window[]=0-200k&\ bucket_width=1h" \ -H "anthropic-version: 2023-06-01" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" ``` #### Filter usage by API keys and workspaces ```bash cURL curl "https://api.anthropic.com/v1/organizations/usage_report/messages?\ starting_at=2025-01-01T00:00:00Z&\ ending_at=2025-01-08T00:00:00Z&\ api_key_ids[]=apikey_01Rj2N8SVvo6BePZj99NhmiT&\ api_key_ids[]=apikey_01ABC123DEF456GHI789JKL&\ workspace_ids[]=wrkspc_01JwQvzr7rXLA5AGx3HKfFUJ&\ workspace_ids[]=wrkspc_01XYZ789ABC123DEF456MNO&\ bucket_width=1d" \ -H "anthropic-version: 2023-06-01" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" ``` To retrieve your organization's API key IDs, use the [List API Keys](https://platform.claude.com/docs/en/api/admin-api/apikeys/list-api-keys) endpoint. To retrieve your organization's workspace IDs, use the [List Workspaces](https://platform.claude.com/docs/en/api/admin-api/workspaces/list-workspaces) endpoint, or find your organization's workspace IDs in the Claude Console. #### Data residency Track your [data residency controls](https://platform.claude.com/docs/en/manage-claude/data-residency) by grouping and filtering usage with the `inference_geo` dimension. This is useful for verifying geographic routing across your organization. ```bash cURL curl "https://api.anthropic.com/v1/organizations/usage_report/messages?\ starting_at=2026-02-01T00:00:00Z&\ ending_at=2026-02-08T00:00:00Z&\ group_by[]=inference_geo&\ group_by[]=model&\ bucket_width=1d" \ -H "anthropic-version: 2023-06-01" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" ``` You can also filter to a specific geo. Valid values are `global`, `us`, and `not_available`: ```bash cURL curl "https://api.anthropic.com/v1/organizations/usage_report/messages?\ starting_at=2026-02-01T00:00:00Z&\ ending_at=2026-02-08T00:00:00Z&\ inference_geos[]=us&\ group_by[]=model&\ bucket_width=1d" \ -H "anthropic-version: 2023-06-01" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" ``` Models released before February 2026 (prior to Claude Opus 4.6 and Claude Sonnet 4.6) don't support the `inference_geo` request parameter, so their usage reports return `"not_available"` for this dimension. You can use `not_available` as a filter value in `inference_geos[]` to target those models. #### Fast mode (research preview) Track [fast mode](https://platform.claude.com/docs/en/build-with-claude/fast-mode) usage by grouping and filtering with the `speed` dimension. This is useful for monitoring standard versus fast mode usage. ```bash cURL curl "https://api.anthropic.com/v1/organizations/usage_report/messages?\ starting_at=2026-02-01T00:00:00Z&\ ending_at=2026-02-08T00:00:00Z&\ group_by[]=speed&\ group_by[]=model&\ bucket_width=1d" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: fast-mode-2026-02-01" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" ``` You can also filter to a specific speed. Valid values are `standard` and `fast`: ```bash cURL curl "https://api.anthropic.com/v1/organizations/usage_report/messages?\ starting_at=2026-02-01T00:00:00Z&\ ending_at=2026-02-08T00:00:00Z&\ speeds[]=fast&\ group_by[]=model&\ bucket_width=1d" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: fast-mode-2026-02-01" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" ``` Both the `speeds[]` filter and the `speed` group\_by value require the `fast-mode-2026-02-01` beta header. ### Time granularity limits | Granularity | Default limit | Maximum limit | Use case | | ----------- | ------------- | ------------- | ---------------------- | | `1m` | 60 buckets | 1,440 buckets | Real-time monitoring | | `1h` | 24 buckets | 168 buckets | Daily patterns | | `1d` | 7 buckets | 31 buckets | Weekly/monthly reports | ## Cost API Retrieve service-level cost breakdowns in USD with the `/v1/organizations/cost_report` endpoint. ### Key concepts * **Currency:** All costs in USD, reported as decimal strings in lowest units (cents) * **Cost types:** Track token usage, web search, and code execution costs * **Grouping:** Group costs by workspace or description for detailed breakdowns. When grouping by `description`, responses include parsed fields such as `model` and `inference_geo` * **Time buckets:** Daily granularity only (`1d`) For complete parameter details and response schemas, see the [Cost API reference](https://platform.claude.com/docs/en/api/admin-api/usage-cost/get-cost-report). Priority Tier costs use a different billing model and are not included in the cost endpoint. Track Priority Tier usage through the usage endpoint instead. ### Basic example ```bash cURL curl "https://api.anthropic.com/v1/organizations/cost_report?\ starting_at=2025-01-01T00:00:00Z&\ ending_at=2025-01-31T00:00:00Z&\ group_by[]=workspace_id&\ group_by[]=description" \ -H "anthropic-version: 2023-06-01" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" ``` ## Pagination Both endpoints support pagination for large datasets: 1. Make your initial request. 2. If `has_more` is `true`, use the `next_page` value in your next request. 3. Continue until `has_more` is `false`. ```bash cURL # First request curl "https://api.anthropic.com/v1/organizations/usage_report/messages?\ starting_at=2025-01-01T00:00:00Z&\ ending_at=2025-01-31T00:00:00Z&\ limit=7" \ -H "anthropic-version: 2023-06-01" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" # Response includes: "has_more": true, "next_page": "page_xyz..." # Next request with pagination curl "https://api.anthropic.com/v1/organizations/usage_report/messages?\ starting_at=2025-01-01T00:00:00Z&\ ending_at=2025-01-31T00:00:00Z&\ limit=7&\ page=page_xyz..." \ -H "anthropic-version: 2023-06-01" \ -H "x-api-key: $ANTHROPIC_ADMIN_KEY" ``` ## Common use cases Explore detailed implementations in [Claude Cookbook](https://platform.claude.com/cookbook): * **Daily usage reports:** Track token consumption trends * **Cost attribution:** Allocate expenses by workspace for chargebacks * **Cache efficiency:** Measure and optimize prompt caching * **Budget monitoring:** Set up alerts for spending thresholds * **CSV export:** Generate reports for finance teams ## Frequently asked questions ### How fresh is the data? Usage and cost data typically appears within 5 minutes of API request completion, though delays may occasionally be longer. ### What's the recommended polling frequency? The API supports polling once per minute for sustained use. For short bursts (for example, downloading paginated data), more frequent polling is acceptable. Cache results for dashboards that need frequent updates. ### How do I track code execution usage? Code execution costs appear in the cost endpoint grouped under `Code Execution Usage` in the description field. Code execution is not included in the usage endpoint. ### How do I track Priority Tier usage? Filter or group by `service_tier` in the usage endpoint and look for the `priority` value. Priority Tier costs are not available in the cost endpoint. ### What happens with Anthropic Workbench usage? API usage from the Workbench is not associated with an API key, so `api_key_id` will be `null` even when grouping by that dimension. ### How is the default workspace represented? Usage and costs attributed to the default workspace have a `null` value for `workspace_id`. ### How do I get per-user cost breakdowns for Claude Code? Use the [Claude Code Analytics API](https://platform.claude.com/docs/en/manage-claude/claude-code-analytics-api), which provides per-user estimated costs and productivity metrics without the performance limitations of breaking down costs by many API keys. For general API usage with many keys, use the [Usage API](https://platform.claude.com/docs/en/manage-claude/usage-cost-api#usage-api) to track token consumption as a cost proxy. ## See also Use the Usage and Cost APIs to deliver a better experience for your users, manage costs, and preserve your rate limit. Learn more about some of these other features: * [Admin API](https://platform.claude.com/docs/en/manage-claude/admin-api) * [Admin API reference](https://platform.claude.com/docs/en/api/admin) * [Analytics APIs](https://platform.claude.com/docs/en/manage-claude/analytics-api) - Which analytics API and key type your organization needs * [Pricing](https://platform.claude.com/docs/en/about-claude/pricing) * [Prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) - Optimize costs with caching * [Batch processing](https://platform.claude.com/docs/en/build-with-claude/batch-processing) - 50% discount on batch requests * [Rate limits](https://platform.claude.com/docs/en/api/rate-limits) - Understand usage tiers * [Rate Limits API](https://platform.claude.com/docs/en/manage-claude/rate-limits-api) - Read your configured rate limits * [Data residency](https://platform.claude.com/docs/en/manage-claude/data-residency) - Control inference geography ### Data & compliance --- title: Access Transparency url: https://platform.claude.com/docs/en/manage-claude/access-transparency description: Receive an audit record of human access to your organization's data by Anthropic personnel through the Compliance API. --- Learn how Access Transparency creates a record of human access to your organization's data by Anthropic personnel, what it covers, and how to receive events through the Compliance API. When Access Transparency is enabled for your organization: * Each human view of your retained data (see [covered content](https://platform.claude.com/docs/en/manage-claude/access-transparency#what-access-transparency-covers)) by an Anthropic employee writes an `anthropic_access` activity to your [Compliance API Activity Feed](https://platform.claude.com/docs/en/manage-claude/compliance-activity-feed). * Access occurs only for safety review or incident response. See [Reason codes](https://platform.claude.com/docs/en/manage-claude/access-transparency#reason-codes). Access Transparency is available to eligible customers on request and is not self-serve. For eligibility, refer to your contract terms or contact your Anthropic account representative. ## How Access Transparency works Anthropic personnel access customer content only under defined conditions. Access Transparency is designed to make such access visible to you. The design rests on the following principles: * **Human access happens only under a published reason code.** * **Human views of your covered content are recorded.** Anthropic's internal tooling that can reach your covered content is instrumented to emit an event on each view. * **Events represent human access, not automated processing.** Anthropic's automated safety systems process your content in a secured pipeline with no interactive human access; that processing does not generate `anthropic_access` events. The one event automated processing can initiate is a `cmek_preserve` preservation record (see [CMEK content preservation](https://platform.claude.com/docs/en/manage-claude/access-transparency#cmek-content-preservation)). * **Events arrive on your existing feed.** Activities are accessible through your [Compliance API Activity Feed](https://platform.claude.com/docs/en/manage-claude/compliance-activity-feed). Existing credentials, audit, export, and SIEM integrations for the Compliance API will still apply. ## What Access Transparency covers * **Covered content:** Access Transparency covers prompt and response content sent through the Claude Messages API or Claude Code sessions. Anthropic's [general ZDR documentation](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention) and [ZDR for Claude Code documentation](https://code.claude.com/docs/en/zero-data-retention) explain which APIs and features are covered by ZDR. The same APIs and features are covered by Access Transparency. * **Manual views by Anthropic personnel:** Manual views of your covered content by Anthropic reviewers generate events. ## What Access Transparency does not cover * **Automated processing:** Model serving, safety classifiers, and abuse-detection pipelines process your content as part of normal operation and do not generate `anthropic_access` events. Preservation initiated by automated processing does generate a `cmek_preserve` event (see [CMEK content preservation](https://platform.claude.com/docs/en/manage-claude/access-transparency#cmek-content-preservation)). * **Your own organization's activity:** Your API calls, admin actions, and Compliance API reads are covered by standard [Activity Feed](https://platform.claude.com/docs/en/manage-claude/compliance-activity-feed) event types. * **Claude for Enterprise and Claude Apps:** claude.ai Enterprise seats, Claude for Work, Cowork, and Claude in Chrome are not covered. * **Claude consumer products:** Claude Free, Pro, or Max plans. * **Partner-operated platforms:** Amazon Bedrock and Google Cloud; refer to those platforms' transparency controls. * **Anything ZDR does not cover:** Products that are not covered by ZDR (for example, the Files API, Anthropic-hosted stateful applications, and the Batch API) are not covered by Access Transparency. See [ZDR documentation](https://code.claude.com/docs/en/zero-data-retention#what-zdr-does-not-cover) for additional details. ## Getting started To enable Access Transparency: Contact your Anthropic account representative. Anthropic confirms your organization meets the eligibility criteria and enables the capability at the organization level. `anthropic_access` activities appear in your existing Activity Feed under your existing Compliance Access Key; no new endpoint or credentials are required. Access Transparency is enabled at the organization level and covers all workspaces. Per-workspace enrollment is not currently available. ## Receiving Access Transparency events Access Transparency events are delivered as the `anthropic_access` activity type on the Compliance API Activity Feed. Filter with `activity_types[]`: ```bash curl --fail-with-body -sS -G \ "https://api.anthropic.com/v1/compliance/activities" \ --data-urlencode "activity_types[]=anthropic_access" \ --data-urlencode "limit=50" \ --header "x-api-key: $ANTHROPIC_COMPLIANCE_ACCESS_KEY" ``` Pagination, date-range filtering (`created_at.gte` / `.lt`), and the response envelope (`has_more`, `first_id`, `last_id`) are shared with the rest of the Activity Feed. See [Query the Activity Feed](https://platform.claude.com/docs/en/manage-claude/compliance-activity-feed). Each `anthropic_access` activity carries the standard Activity fields plus the following: | Field | Type | Description | | ------------------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | string | Unique identifier for this activity | | `accessed_at` | RFC 3339 string | When the access occurred. Might be earlier than when the activity becomes visible in your feed | | `created_at` | RFC 3339 string | When the activity became visible in your feed | | `actor` | object | Always `{ "type": "anthropic_actor", "email_address": null }`. Individual employee identity is not disclosed | | `accessor_department` | string | The Anthropic team that performed the access (for example, `Safeguards`) | | `reason_code` | enum | See [Reason codes](https://platform.claude.com/docs/en/manage-claude/access-transparency#reason-codes) | | `resource_details.type` | enum | A resource type, currently only `message`. Extensible for future resource types | | `resource_details.id` | string or null | Identifier of the content accessed | | `resource_details.parent` | string or null | Identifier of the content's parent, for example the conversation ID containing a message. Currently `null` or omitted until resources with parents are supported | | `organization_id` | string | The organization the content belongs to. Tagged ID format (`org_...`) | | `organization_uuid` | string | The organization the content belongs to. UUID format | | `workspace_id` | string or null | The workspace the content belongs to | Example JSON message: ```json { "id": "activity_013b013744txqZtFHLUaRqLr", "type": "anthropic_access", "created_at": "2026-06-08T17:12:09.812446Z", "accessed_at": "2026-06-08T17:12:06.478035Z", "organization_id": "org_0910d9133038914eta7i3vt", "actor": { "type": "anthropic_actor", "email_address": null }, "resource_details": { "type": "message", "id": "msg_1234ABCD" }, "accessor_department": "Safeguards", "reason_code": "safety_review", "organization_uuid": "5b236db4-3fb4-4bf3-a560-b5e266038a15" } ``` ## CMEK content preservation In rare cases, Anthropic preserves specific content beyond the standard retention window (for example, when a safety review confirms severely harmful content that must be retained for an ongoing investigation). Preservation is itself a logged, customer-visible action: * **A preservation event is written to your feed.** When content is preserved, an event with type `cmek_preserve` is written to your Compliance API Activity Feed. Preservation events carry the same fields as an `anthropic_access` event; only the event type differs, so a parser that handles one handles both. See [Reason codes](https://platform.claude.com/docs/en/manage-claude/access-transparency#reason-codes). * **A preservation event is written regardless of how the preservation was initiated.** Preservation ordinarily follows human review of the content, but the event is written whether the preservation was initiated by a human reviewer or by an automated safety pipeline: the record reflects that your content's retention state changed, independent of who changed it. * **For CMEK organizations, preservation is a visible key movement.** Preserved content is re-encrypted outside your customer-managed key so that the investigation can continue independent of your key. The preservation event is your record that this occurred. All other retained content remains under your key. Filter for preservation events the same way as access events: ```bash curl --fail-with-body -sS -G \ "https://api.anthropic.com/v1/compliance/activities" \ --data-urlencode "activity_types[]=cmek_preserve" \ --data-urlencode "limit=50" \ --header "x-api-key: $ANTHROPIC_COMPLIANCE_ACCESS_KEY" ``` Example JSON message: ```json { "id": "activity_01AbCdEfGhJkMnPqRsTuVwXy", "type": "cmek_preserve", "created_at": "2026-07-02T09:41:53.204118Z", "accessed_at": "2026-07-02T09:41:50.118764Z", "organization_id": "org_0123456789abcdefghijklmn", "actor": { "type": "anthropic_actor", "email_address": null }, "resource_details": { "type": "message", "id": "msg_0ExampleExampleExample" }, "accessor_department": "Safeguards", "reason_code": "policy_violation_investigation", "organization_uuid": "00000000-1111-2222-3333-444444444444" } ``` For preservation events, `accessed_at` records when the content was preserved. ## Reason codes The set of reason codes is closed. Anthropic will update this page in the event it introduces a new code. | Code | Meaning | | -------------------------------- | ------------------------------------------------------------------------------ | | `safety_review` | Content was viewed as part of a usage-policy or safety investigation | | `incident_response` | Content was viewed while investigating an incident affecting your organization | | `policy_violation_investigation` | Content was preserved during a Trust and Safety policy-violation investigation | | `csae_report` | Content was preserved as evidence for a child safety (CSAE) report | ## Surface eligibility The following table lists which surfaces are covered by Access Transparency. Coverage means human access to content from that surface generates `anthropic_access` events. | Surface | Covered | Details | | ----------------------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------- | | Claude API (`api.anthropic.com`) | Yes | Prompts, completions, and data directly embedded in the API inputs | | Claude Code (using an API key) | Yes | API traffic from Claude Code is covered as Claude API traffic | | Claude Platform on AWS | Yes | Claude Platform on AWS generates Access Transparency events within the Compliance API (not AWS CloudTrail) | | Claude API (`api.anthropic.com`) (Batch, Files) | No | The Claude API Batch and Files APIs are not covered, just like they are not covered by ZDR | | Claude for Enterprise (claude.ai seats) | No | Not covered | | Claude for Work | No | Not covered | | Claude Free, Pro, Max | No | Consumer plans are not eligible | | Anthropic Workbench | No | The Workbench stores data in data stores that are not covered by Access Transparency | | Microsoft Foundry | No | Not available | | Amazon Bedrock, Google Cloud | No | Partner-operated platforms; refer to those platforms' transparency controls | ## Limitations and exclusions ### Coverage timing Access Transparency applies from the time it is enabled for your organization. Content already in your retention window at enablement might also generate events when accessed, but Anthropic does not guarantee coverage for content written before enablement. Treat your enablement date as the start of reliable coverage. There might be a delay of up to two hours between enabling Access Transparency and your content being covered. ### Notification timing `anthropic_access` and `cmek_preserve` events are delivered to your Compliance API feed within two business days of the access or preservation they record. This feed should not be treated as a real-time alerting channel, and the `accessed_at` timestamp reflects when the access occurred, which might be up to two business days before the activity becomes visible in your feed. The `created_at` field reflects the time that the event became visible. ### Automated processing does not generate access events `anthropic_access` events record human access only. Anthropic's automated safety systems and classifiers continue to process your content as part of normal operation, and that processing does not generate `anthropic_access` events. The one event automated processing can initiate is a `cmek_preserve` preservation record (see [CMEK content preservation](https://platform.claude.com/docs/en/manage-claude/access-transparency#cmek-content-preservation)). An empty feed means no human at Anthropic has viewed your content; it does not mean your content was not processed by automated systems. ### Access Transparency does not change what Anthropic can access Access Transparency records access; it does not grant or restrict it. The purposes for which Anthropic personnel may access your content are governed by your agreement with Anthropic and the [Usage Policies](https://www.anthropic.com/legal/aup), and are the same regardless of whether Access Transparency is enabled. ### CMEK key-use logs are not a per-read record For organizations that also enable CMEK, your cloud KMS audit log (CloudTrail, Cloud Audit Logs, or Azure Monitor) records Anthropic's use of your key. Because keys are cached for short periods during operation, an individual human read does not necessarily produce a distinct KMS decryption entry. Use the Access Transparency feed as the per-access record; your KMS log independently confirms key usage patterns. ## Frequently asked questions Contact your Anthropic account representative. No. Automated processing does not generate `anthropic_access` events; you will see an `anthropic_access` event only if a human reviewer subsequently views the content. Separately, a `cmek_preserve` event is written when content is preserved, whether the preservation was initiated by a human reviewer or an automated safety pipeline. Access Transparency is not available for platform deployments. Contact your Anthropic account representative to discuss your use case. Access Transparency is not guaranteed to be retroactive. It covers human access to content written to the Claude API on or after your enrollment date. You might see events for access to content that was written before enrollment. Within two business days of the access. Configure any SIEM alerting or scheduled exports with a matching lookback window rather than assuming real-time arrival. Use the `resource_details.id` field. It contains the same message ID (`msg_...`) that the [Messages API](https://platform.claude.com/docs/en/api/messages/create) returns in the `id` field of every response body. To make this useful, log `id` in your own systems alongside your internal metadata, such as the application, end user, or conversation that produced the request. When an event arrives, join its `resource_details.id` against your logs to identify exactly which request was viewed. Access Transparency is enabled at the organization level and covers all workspaces. They are independent. With CMEK, safety preservation outside your key emits a separate `cmek_preserve` event on the same feed. See [CMEK content preservation](https://platform.claude.com/docs/en/manage-claude/access-transparency#cmek-content-preservation) and [CMEK](https://platform.claude.com/docs/en/manage-claude/cmek). Contact your Anthropic account representative. ## Related resources * [Compliance API overview](https://platform.claude.com/docs/en/manage-claude/compliance-api) * [Activity Feed](https://platform.claude.com/docs/en/manage-claude/compliance-activity-feed) * [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention) * [Customer-Managed Encryption Keys (CMEK)](https://platform.claude.com/docs/en/manage-claude/cmek) * [Claude Code data usage](https://code.claude.com/docs/en/data-usage) * [Trust Center](https://trust.anthropic.com/resources) --- title: API and data retention url: https://platform.claude.com/docs/en/manage-claude/api-and-data-retention description: Learn about how Anthropic's APIs and associated features retain data, including information about zero data retention (ZDR) and HIPAA-ready API access. --- This page covers the Claude API (`api.anthropic.com`), Claude Platform on AWS, and [Claude in Microsoft Foundry](https://platform.claude.com/docs/en/build-with-claude/claude-in-microsoft-foundry), where Anthropic is the data processor. On Amazon Bedrock and Google Cloud's Agent Platform, the cloud provider is the data processor; refer to those platforms' data retention and compliance documentation for their equivalent controls. Anthropic offers two data handling arrangements for the Claude API: [zero data retention (ZDR)](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#zero-data-retention-zdr-scope) and [HIPAA readiness](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#hipaa-readiness). The [feature eligibility table](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#feature-eligibility) lists which API features each arrangement covers. For Anthropic's standard retention policies outside these arrangements, see the [commercial data retention policy](https://privacy.claude.com/en/articles/7996866-how-long-do-you-store-my-organization-s-data) and the [consumer data retention policy](https://privacy.claude.com/en/articles/10023548-how-long-do-you-store-my-data). ## How Anthropic approaches data retention Different APIs and features have different storage needs. Where a feature does not require storage of customer prompts or responses, it may be eligible for ZDR. Where a feature necessarily requires storage, Anthropic designs for the smallest possible retention footprint under the following commitments: * Retained data is never used for model training without your express permission. * Only what is technically necessary for the feature to work is retained. Conversation content (your prompts and Claude's outputs) is not retained by default; the exception is [Covered Models](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#model-specific-data-retention-requirements), which require 30-day retention. * Retained data is purged on the shortest practical time to live (TTL), and Anthropic aims to give customers control over how long data is retained. What is held, and the retention duration where a specific TTL applies, is documented on each feature's page. Several retention models sit outside the ZDR and HIPAA arrangements described on this page. Data accessible through the [Compliance API](https://platform.claude.com/docs/en/manage-claude/compliance-api) follows its own retention model. The [Activity Feed](https://platform.claude.com/docs/en/manage-claude/compliance-activity-feed) retains data for 6 years. Chat, file, and project content from claude.ai follows your organization's retention policy set in [claude.ai > Organization settings > Data and privacy](https://claude.ai/admin-settings/data-privacy-controls). [Local session transcripts](https://platform.claude.com/docs/en/manage-claude/compliance-sessions#retrieve-local-sessions) (Cowork and Claude Code on users' machines) are stored for 6 years by default, or for your organization's custom conversation retention period when a finite one is set (the same claude.ai setting). [Remote session transcripts](https://platform.claude.com/docs/en/manage-claude/compliance-sessions#retrieve-remote-sessions) (Cowork in the cloud) are retained for 6 years. The Compliance API does not capture local sessions for which ZDR is in effect, or any local sessions from organizations with HIPAA readiness enabled. ## Zero data retention (ZDR) Under a ZDR arrangement, Anthropic does not store customer prompts or responses at rest after the API response is returned. To request ZDR for your organization, contact the [Anthropic sales team](https://claude.com/contact-sales). ZDR is enabled per organization; each new organization requires ZDR to be enabled separately by your account team, and enablement does not automatically extend to other organizations under the same account. ### What ZDR covers * **Claude Messages and Token Counting APIs:** ZDR applies to these endpoints for eligible features listed in the [feature eligibility table](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#feature-eligibility). Features that ride on `/v1/messages` but are marked "No" in the table (such as code execution) are not covered. * **Claude Code:** ZDR applies when Claude Code is used with API keys from a Commercial organization (an organization under Anthropic's Commercial Terms of Service, as distinct from a consumer Claude account) or through Claude Enterprise with ZDR enabled. If metrics logging is enabled in Claude Code, productivity data such as usage statistics is exempted from ZDR and may be retained. See the [Claude Code ZDR documentation](https://code.claude.com/docs/en/zero-data-retention) for full details. * **Claude Platform on AWS:** [Claude Platform on AWS](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws) follows the same data retention policy as the first-party Claude API. ZDR is available on request; contact your Anthropic account representative to enable it. ### What ZDR does not cover * **Console and Workbench:** Any usage on Claude Console or the Workbench prompt-testing interface. * **Claude Managed Agents:** Claude Managed Agents is a stateful resource; session transcripts persist until you delete them. * **Claude consumer products:** Claude Free, Pro, and Max plans, including when customers on those plans use Claude's web, desktop, or mobile apps or Claude Code. * **Claude Teams and Claude Enterprise product interfaces:** These interfaces are not ZDR-eligible. The exception is Claude Code used through Claude Enterprise with ZDR enabled; see [What ZDR covers](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#what-zdr-covers). * **Claude for Excel:** Not currently ZDR-eligible. * **Claude Fable 5 and Claude Mythos 5:** These models require 30-day data retention and are not available under ZDR. See [Model-specific data retention requirements](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#model-specific-data-retention-requirements). * **Third-party integrations:** Data processed by third-party websites, tools, or other integrations is not covered, though some may have similar offerings. Review each service's data handling practices. * **Cross-Origin Resource Sharing (CORS):** CORS is not supported for organizations with ZDR arrangements. To make API calls from browser-based applications, route requests through a backend proxy server. See the [API security guidance](https://platform.claude.com/docs/en/api/overview) for proxy patterns and API-key handling. * **Flagged content and legal holds:** See [Retention regardless of arrangement](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#retention-regardless-of-arrangement). For the most up-to-date information on which products and features are ZDR-eligible, refer to your contract terms or contact your Anthropic account representative. ## HIPAA readiness The Claude API supports HIPAA-ready integrations for organizations that handle protected health information (PHI). With a signed BAA and a HIPAA-enabled organization, you can use supported API features to process PHI while supporting your organization's HIPAA compliance. Eligible organizations can review and execute the BAA and enable HIPAA readiness directly from the Claude Console. HIPAA readiness applies a broader set of privacy and security safeguards than ZDR (encryption, access controls, and audit logging that protect PHI throughout its lifecycle) rather than requiring immediate deletion. If your organization handles PHI, HIPAA readiness is the arrangement to use; you do not also need ZDR. See the [feature eligibility table](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#feature-eligibility) for which features each arrangement covers. This page covers HIPAA readiness for the Claude API. For the full HIPAA Implementation Guide covering Claude Enterprise and configuration requirements, see the [Anthropic Trust Center](https://trust.anthropic.com/resources). ### What HIPAA readiness covers * **Claude API:** HIPAA readiness applies to the Claude API (`api.anthropic.com`) for eligible features listed in the [feature eligibility table](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#feature-eligibility). ### What HIPAA readiness does not cover * **Claude consumer products:** Claude Free, Pro, and Max plans. * **Console and Workbench:** Usage through the Claude Console interface (enabling HIPAA readiness from Console settings is supported; processing PHI through the Console is not covered). * **Partner-operated platforms:** Amazon Bedrock and Google Cloud's Agent Platform. Refer to those platforms' compliance documentation. * **Claude Platform on AWS and Microsoft Foundry:** HIPAA readiness is not available on these platforms. * **Third-party integrations:** Data processed by external tools or services connected to your application. * **Claude Code:** Claude Code is not covered under HIPAA readiness. * **Beta features:** Features in beta are generally not covered under the BAA unless explicitly listed as eligible in the [feature eligibility table](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#feature-eligibility). * **Flagged content and legal holds:** See [Retention regardless of arrangement](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#retention-regardless-of-arrangement). ### PHI handling guidelines Protected health information (PHI) includes any individually identifiable health information. In the context of the Claude API, PHI typically appears in message content (prompts and Claude's responses), attached files (images, PDFs), and file names or metadata associated with message content. The following fields are not expected to contain PHI under the BAA: workspace names, user information (name, email, phone number), billing data, and support tickets. When using [structured outputs](https://platform.claude.com/docs/en/build-with-claude/structured-outputs) or tools with `strict: true`, the API compiles JSON schemas into grammars that are cached separately from message content. These cached schemas do not receive the same PHI protections as prompts and responses. **Do not include PHI in JSON schema definitions.** This restriction applies to schema property names, `enum` values, `const` values, and `pattern` regular expressions. Patient-specific information should appear only in message content, where it is protected under HIPAA safeguards. ### HIPAA error handling Your signed BAA is the official source of truth for which features are covered. The API also enforces these restrictions automatically. When a HIPAA-enabled organization sends a request that includes a non-eligible feature, the API returns a `400` error to prevent accidental use of features not covered by your BAA: ```json { "type": "error", "error": { "type": "invalid_request_error", "message": "The requested features are not available for HIPAA-regulated organizations without Zero Data Retention: code_execution." } } ``` The error message lists the non-eligible features detected in the request; remove them and retry. The phrase "without Zero Data Retention" is the API's own wording and does not change the resolution. ### Getting started with HIPAA readiness There are two ways to set up HIPAA-ready API access. Most organizations can enable it directly in the Claude Console with Anthropic's standard BAA; organizations that require a negotiated BAA should work with their account team. #### Enable in the Console (standard BAA) In [Claude Console > Settings > Privacy](https://platform.claude.com/settings/privacy), organization admins with the HIPAA management permission see a **HIPAA compliance** card. If your organization is eligible but you don't see the option to enable, ask an organization admin to complete these steps. Download the Business Associate Agreement and the HIPAA Implementation Guide, then accept the agreement as an authorized legal representative of your organization. Each step becomes available after you download the prior document, and your enablement is bound to the exact BAA version you downloaded. HIPAA readiness controls are applied to your organization as soon as you accept. Once HIPAA readiness is enabled for your organization, the configuration is permanent and cannot be disabled by an administrator. The API automatically enforces feature restrictions, returning an error for requests that use non-eligible features. See [HIPAA error handling](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#hipaa-error-handling). #### Contact sales (custom BAA) If your organization requires a negotiated or custom BAA, or if self-serve enablement isn't available for your organization, contact the [Anthropic sales team](https://claude.com/contact-sales). Anthropic will execute the BAA and enable HIPAA readiness for your organization. #### Build with eligible features Whichever path you use, confirm which features are supported in the [feature eligibility table](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#feature-eligibility) and review the [PHI handling guidelines](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#phi-handling-guidelines) for features that restrict where PHI can appear. For detailed configuration and compliance requirements, refer to the [HIPAA Implementation Guide](https://trust.anthropic.com/resources). HIPAA readiness is enforced at the organization level. If you need both HIPAA-ready and general-purpose API access, use separate organizations for each. ## Model-specific data retention requirements Claude Fable 5 and Claude Mythos 5 are designated Covered Models (see the [Covered Models support article](https://support.claude.com/en/articles/15425695)) and require 30-day data retention; ZDR is therefore not available for either model. On the Claude API, requests to Claude Fable 5 from an organization whose data retention configuration does not meet this requirement return a `400 invalid_request_error`: ```json { "type": "error", "error": { "type": "invalid_request_error", "message": "In order to access this model, your organization or workspace must have data retention enabled." } } ``` The 30-day data retention requirement applies wherever Covered Models are offered. On the Claude API (including Claude Platform on AWS), Anthropic handles retained data. On Amazon Bedrock and Google Cloud's Agent Platform, retained data stays within your cloud provider's environment; review each platform's documentation for enablement steps. ### Enable 30-day retention for a workspace Organizations with a ZDR arrangement can make Claude Fable 5 and Claude Mythos 5 available in a specific workspace by enabling 30-day retention for that workspace only. Other workspaces in the organization keep zero data retention. In [Claude Console > Settings > Workspaces](https://platform.claude.com/settings/workspaces), select the workspace and open its **Privacy controls** tab. Enable the 30-day data retention setting for the workspace. Requests to Claude Fable 5 and Claude Mythos 5 from this workspace now succeed. Workspaces without an override continue to follow the organization default. ## Feature eligibility The following table lists which Claude API features are eligible for ZDR and HIPAA readiness arrangements. Each eligibility column uses three values: * **Yes:** The feature is fully eligible under the arrangement. For ZDR, "Yes" also assumes you are using a model that does not require 30-day data retention; [Covered Models](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#model-specific-data-retention-requirements) are not available under ZDR regardless of feature eligibility. * **Yes (qualified):** Your prompts and Claude's outputs are not stored, but a bounded technical artifact (named in the Details column) is retained briefly for the feature to function. See [How Anthropic approaches data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#how-anthropic-approaches-data-retention) for the commitments that govern these features. * **No:** The feature is not eligible. Under HIPAA readiness, the API blocks requests that include a "No" feature and returns a `400` error. Under ZDR, the API does **not** block these features; using one is a choice to step outside your ZDR arrangement for that specific data, and the feature's own documented retention policy applies. Features marked "No" for ZDR are typically stateful (they store jobs, files, or container state), which is why they cannot be zero-retention. | Feature | Endpoint | ZDR eligible | HIPAA eligible | Details | | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | ------------------------------------------------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [1M token context window](https://platform.claude.com/docs/en/build-with-claude/context-windows) | `/v1/messages` | Yes | Yes | | | [Adaptive thinking](https://platform.claude.com/docs/en/build-with-claude/thinking) | `/v1/messages` | Yes | Yes | | | [Advisor tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/advisor-tool) | `/v1/messages` (with `advisor` tool) | Yes | No | Advisor model output is returned in the API response; nothing is stored server-side after the response. | | [Agent skills](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview) | `/v1/messages` (with `skills`) / `/v1/skills` | No | No | Skill data retained per standard policy. See [Agent skills](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview#data-retention). | | [Bash tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/bash-tool) | `/v1/messages` (with `bash` tool) | Yes | Yes | Client-side tool executed in your environment. | | [Batch processing](https://platform.claude.com/docs/en/build-with-claude/batch-processing) | `/v1/messages/batches` | No | No | 29-day retention; async storage required. See [Batch processing](https://platform.claude.com/docs/en/build-with-claude/batch-processing#data-retention). | | [Cache diagnostics](https://platform.claude.com/docs/en/build-with-claude/cache-diagnostics) | `/v1/messages` (with `diagnostics`) | Yes (qualified) | No | Your prompts and Claude's outputs are not stored. A fingerprint of cryptographic hashes and token-count estimates is retained briefly to enable comparison against the next request. See [Cache diagnostics](https://platform.claude.com/docs/en/build-with-claude/cache-diagnostics#data-retention). | | [Citations](https://platform.claude.com/docs/en/build-with-claude/citations) | `/v1/messages` | Yes | Yes | | | [Claude Managed Agents](https://platform.claude.com/docs/en/managed-agents/overview) | `/v1/agents`, `/v1/sessions`, `/v1/environments` | No | No | Sessions are stateful resources; transcripts persist until you delete them. Applies to all Managed Agents sub-features, including [Self-hosted sandboxes](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes). | | [Code execution](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool) | `/v1/messages` (with `code_execution` tool) | No | No | Container data retained up to 30 days. See [Code execution](https://platform.claude.com/docs/en/agents-and-tools/tool-use/code-execution-tool#data-retention). | | [Computer use](https://platform.claude.com/docs/en/agents-and-tools/tool-use/computer-use-tool) | `/v1/messages` (with `computer` tool) | Yes | Yes | Client-side tool where screenshots and files are captured and stored in your environment, not by Anthropic. See [Computer use](https://platform.claude.com/docs/en/agents-and-tools/tool-use/computer-use-tool#data-retention). | | [Context editing](https://platform.claude.com/docs/en/build-with-claude/context-editing) | `/v1/messages` (with `context_management`) | Yes | No | Context edits (tool use clearing and thinking clearing) are applied in real time. | | [Context management (compaction)](https://platform.claude.com/docs/en/build-with-claude/compaction) | `/v1/messages` (with `context_management`) | Yes | No | Server-side compaction results are returned and round-tripped statelessly through the API response. | | [Data residency](https://platform.claude.com/docs/en/manage-claude/data-residency) | `/v1/messages` (with `inference_geo`) | Yes | Yes | | | [Effort](https://platform.claude.com/docs/en/build-with-claude/effort) | `/v1/messages` (with `effort`) | Yes | Yes | | | [Fast mode](https://platform.claude.com/docs/en/build-with-claude/fast-mode) | `/v1/messages` (with `speed: "fast"`) | Yes | Yes | Same Messages API endpoint with faster inference. ZDR applies regardless of speed setting. | | [Files API](https://platform.claude.com/docs/en/build-with-claude/files) | `/v1/files` | No | No | Files retained until explicitly deleted. See [Files API](https://platform.claude.com/docs/en/build-with-claude/files#data-retention). | | [Fine-grained tool streaming](https://platform.claude.com/docs/en/agents-and-tools/tool-use/fine-grained-tool-streaming) | `/v1/messages` | Yes | Yes | | | [MCP connector](https://platform.claude.com/docs/en/agents-and-tools/mcp-connector) | `/v1/messages` (with `mcp_servers`) | No | No | Data retained per standard policy. See [MCP connector](https://platform.claude.com/docs/en/agents-and-tools/mcp-connector#data-retention). | | [MCP tunnels](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/overview) | `/v1/tunnels` | No | No | Research preview. See [MCP tunnels security](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/security) for the data-flow boundary and subprocessor details. | | [Memory tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/memory-tool) | `/v1/messages` (with `memory` tool) | Yes | Yes | Client-side memory storage where you control data retention. | | [Messages API](https://platform.claude.com/docs/en/build-with-claude/working-with-messages) | `/v1/messages` | Yes | Yes | Standard API calls for generating Claude responses. | | [Mid-conversation system messages](https://platform.claude.com/docs/en/build-with-claude/mid-conversation-system-messages) | `/v1/messages` (with `role: "system"` messages) | Yes | Yes | Request-shape capability of the Messages API; mid-conversation system messages flow through the standard inference path and nothing is stored server-side after the response. | | [PDF support](https://platform.claude.com/docs/en/build-with-claude/pdf-support) | `/v1/messages` | Yes | Yes | HIPAA eligibility applies to PDFs sent inline through the Messages API, not through the Files API. | | [Programmatic tool calling](https://platform.claude.com/docs/en/agents-and-tools/tool-use/programmatic-tool-calling) | `/v1/messages` (with `code_execution` tool) | No | No | Built on code execution containers; data retained up to 30 days. See [Programmatic tool calling](https://platform.claude.com/docs/en/agents-and-tools/tool-use/programmatic-tool-calling#data-retention). | | [Prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) | `/v1/messages` | Yes | Yes | Your prompts and Claude's outputs are not stored. KV cache representations and cryptographic hashes are held in memory for the cache TTL and promptly deleted after expiry. See [Prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching#data-retention). | | [Search results](https://platform.claude.com/docs/en/build-with-claude/search-results) | `/v1/messages` (with `search_results` source) | Yes | Yes | | | [Structured outputs](https://platform.claude.com/docs/en/build-with-claude/structured-outputs) | `/v1/messages` | Yes (qualified) | Yes | Your prompts and Claude's outputs are not stored. Only the JSON schema is cached, for up to 24 hours since last use. This also covers [strict tool use](https://platform.claude.com/docs/en/agents-and-tools/tool-use/strict-tool-use) (`strict: true` on tools), which uses the same grammar pipeline. PHI must not be included in JSON schema definitions; see [PHI handling guidelines](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#phi-handling-guidelines). See [Structured outputs](https://platform.claude.com/docs/en/build-with-claude/structured-outputs#data-retention). | | [Text editor tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/text-editor-tool) | `/v1/messages` (with `text_editor` tool) | Yes | Yes | Client-side tool executed in your environment. | | [Thinking](https://platform.claude.com/docs/en/build-with-claude/thinking) | `/v1/messages` (with `thinking`) | Yes | Yes | | | [Token counting](https://platform.claude.com/docs/en/build-with-claude/token-counting) | `/v1/messages/count_tokens` | Yes | Yes | Count tokens before sending requests. | | [Tool search](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool) | `/v1/messages` (with `tool_search` tool) | Yes | No | Server-side tool executed by Anthropic; the tool definitions in the request are searched in memory per call and nothing is stored after the response. | | [Web fetch](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-fetch-tool) | `/v1/messages` (with `web_fetch` tool) | Yes | No | Fetched web content returned in the API response. [Dynamic filtering](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-search-tool#dynamic-filtering) is not eligible for ZDR or HIPAA. Website publishers may retain request data (such as fetched URLs and request metadata) according to their own policies. | | [Web search](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-search-tool) | `/v1/messages` (with `web_search` tool) | Yes | Yes | Real-time web search results returned in the API response. [Dynamic filtering](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-search-tool#dynamic-filtering) is not eligible for ZDR or HIPAA. | ## Retention regardless of arrangement Even with ZDR or HIPAA arrangements in place, Anthropic may retain data where required by law or where it has been flagged by Anthropic's automated trust and safety systems. As a result, if a chat or session is flagged, Anthropic may retain inputs and outputs for up to 2 years. ## Frequently asked questions Check your contract terms or contact your Anthropic account representative to confirm whether your organization has ZDR arrangements in place. Yes. These features retain a minimal, documented set of technical data, not your prompts or Claude's outputs. See the [feature eligibility table](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#feature-eligibility) legend for what "Yes (qualified)" means and [How Anthropic approaches data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#how-anthropic-approaches-data-retention) for the commitments that govern these features. Nothing blocks the request. Features marked "No" for ZDR are fundamentally stateful: the Batch API stores your jobs, the Files API stores your files, and code execution runs in persistent containers. Data for these features is retained per the feature's documented policy. Using them is a choice to step outside your ZDR arrangement for that specific data. Contact your Anthropic account representative to discuss deletion options for non-ZDR features. ZDR prevents customer data from being stored at rest after the API response is returned. HIPAA readiness involves a broader set of privacy and security safeguards that protect PHI throughout its lifecycle, including encryption, access controls, and audit logging. Under HIPAA readiness, data can be retained with these safeguards in place rather than requiring immediate deletion. The two arrangements cover different feature sets; see the [feature eligibility table](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#feature-eligibility). No. HIPAA-ready API access is designed as an alternative to ZDR for organizations handling PHI. With HIPAA readiness enabled, you get access to supported API features while maintaining the privacy and security protections that HIPAA requires. The API returns a `400` error with an `invalid_request_error` type. The error message identifies which features are not available. Remove the non-eligible features from your request and retry. See [HIPAA error handling](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#hipaa-error-handling). No. HIPAA readiness is enforced at the organization level and automatically blocks all non-eligible features. Use a separate organization for workloads that do not require HIPAA readiness. Eligible organizations can enable HIPAA readiness directly in [Claude Console > Settings > Privacy](https://platform.claude.com/settings/privacy) by reviewing and executing Anthropic's standard BAA; see [Getting started with HIPAA readiness](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#getting-started-with-hipaa-readiness). If your organization requires a negotiated BAA, or self-serve enablement isn't available for your organization, contact the [Anthropic sales team](https://claude.com/contact-sales). No. The ZDR and HIPAA arrangements described on this page apply to the Claude API, where Anthropic is the data processor. On Bedrock and Google Cloud, the cloud provider is the data processor; refer to those platforms' data retention and compliance policies for their equivalent controls. Claude Platform on AWS follows the same data retention policy as the first-party Claude API. ZDR is available on request; contact your Anthropic account representative to enable it. HIPAA readiness is not available on Claude Platform on AWS. See [Claude Platform on AWS](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws) for details. Claude Code is eligible for ZDR through two paths: * **API keys:** Claude Code used with pay-as-you-go API keys from a Commercial organization * **Claude Enterprise:** Claude Code used through Claude Enterprise with ZDR enabled for the organization ZDR is enabled on a per-organization basis. Each new organization requires ZDR to be enabled separately by your account team. ZDR does not automatically apply to new organizations created under the same account. Additionally, if you have metrics logging enabled in Claude Code, productivity data (such as usage statistics) is exempted from ZDR and may be retained. For full details on ZDR for Claude Code on Claude Enterprise, including disabled features and how to request enablement, see the [Claude Code ZDR documentation](https://code.claude.com/docs/en/zero-data-retention). No, Claude for Excel is not currently ZDR-eligible. To request a ZDR arrangement, contact the [Anthropic sales team](https://claude.com/contact-sales). ## Related resources * [Privacy Policy](https://www.anthropic.com/legal/privacy) * [Structured outputs](https://platform.claude.com/docs/en/build-with-claude/structured-outputs) * [Prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) * [Batch processing](https://platform.claude.com/docs/en/build-with-claude/batch-processing) * [Files API reference](https://platform.claude.com/docs/en/api/beta/files/upload) * [Trust Center](https://trust.anthropic.com/resources) --- title: Data residency url: https://platform.claude.com/docs/en/manage-claude/data-residency description: Manage where model inference runs and where data is stored with geographic controls. --- Data residency controls let you manage where your data is processed and stored. Two independent settings govern this: * **Inference geo:** Controls where model inference runs, on a per-request basis. Set through the `inference_geo` API parameter or as a workspace default. * **Workspace geo:** Controls where data is stored at rest and where endpoint processing (such as image transcoding and code execution) happens. Configured at the workspace level in the [Claude Console](https://platform.claude.com). [Claude Managed Agents](https://platform.claude.com/docs/en/managed-agents/overview) supports geographic pinning at the agent level: `inference_geo` on an [agent's model configuration](https://platform.claude.com/docs/en/managed-agents/agent-setup#pin-the-inference-geo) pins the geography that serves model requests for sessions running that agent, with [per-session overrides](https://platform.claude.com/docs/en/managed-agents/sessions#pin-the-inference-geo-for-a-session) at session create. Agents without a pin follow the workspace's default inference geo on each request. Managed Agents also respects the Workspace geo configured in Console, and with [self-hosted sandboxes](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes), tool execution and the sandbox filesystem stay on infrastructure you control. ## Inference geo For how zero data retention (ZDR) applies to this feature, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention). The `inference_geo` parameter controls where model inference runs for a specific API request. Add it to any `POST /v1/messages` call. | Value | Description | | ---------- | ----------------------------------------------------------------------------------------------- | | `"global"` | Default. Inference may run in any available geography for optimal performance and availability. | | `"us"` | Inference runs only in US-based infrastructure. | ### API usage ```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHROPIC_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "inference_geo": "us", "messages": [{ "role": "user", "content": "Summarize the key points of this document." }] }' ``` ```bash CLI ant messages create \ --model claude-opus-5 \ --max-tokens 1024 \ --inference-geo us \ --message '{role: user, content: "Summarize the key points of this document."}' \ --transform '{content.#(type=="text").text,usage.inference_geo}' --format yaml ``` ```python Python client = anthropic.Anthropic() response = client.messages.create( model="claude-opus-5", max_tokens=1024, inference_geo="us", messages=[ {"role": "user", "content": "Summarize the key points of this document."} ], ) for block in response.content: if block.type == "text": print(block.text) # Check where inference actually ran print(f"Inference geo: {response.usage.inference_geo}") ``` ```typescript TypeScript const client = new Anthropic(); const response = await client.messages.create({ model: "claude-opus-5", max_tokens: 1024, inference_geo: "us", messages: [ { role: "user", content: "Summarize the key points of this document." } ] }); const textBlock = response.content.find( (block): block is Anthropic.TextBlock => block.type === "text" ); console.log(textBlock?.text); // Check where inference actually ran console.log(`Inference geo: ${response.usage.inference_geo}`); ``` ```csharp C# var client = new AnthropicClient(); var response = await client.Messages.Create( new MessageCreateParams { Model = Model.ClaudeOpus5, MaxTokens = 1024, InferenceGeo = "us", Messages = [ new() { Role = Role.User, Content = "Summarize the key points of this document." }, ], } ); foreach (var block in response.Content) { if (block.TryPickText(out var textBlock)) { Console.WriteLine(textBlock.Text); } } // Check where inference actually ran Console.WriteLine($"Inference geo: {response.Usage.InferenceGeo}"); ``` ```go Go client := anthropic.NewClient() message, err := client.Messages.New(context.Background(), anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus5, MaxTokens: 1024, InferenceGeo: anthropic.String("us"), Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Summarize the key points of this document.")), }, }) if err != nil { log.Fatal(err) } for _, block := range message.Content { if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok { fmt.Println(textBlock.Text) } } // Check where inference actually ran fmt.Printf("Inference geo: %s\n", message.Usage.InferenceGeo) ``` ```java Java AnthropicClient client = AnthropicOkHttpClient.fromEnv(); Message response = client.messages().create( MessageCreateParams.builder() .model(Model.CLAUDE_OPUS_5) .maxTokens(1024L) .inferenceGeo("us") .addUserMessage("Summarize the key points of this document.") .build()); response.content().stream() .flatMap(block -> block.text().stream()) .forEach(textBlock -> IO.println(textBlock.text())); // Check where inference actually ran IO.println("Inference geo: " + response.usage().inferenceGeo().get()); ``` ```php PHP $client = new Client(); $response = $client->messages->create( model: 'claude-opus-5', maxTokens: 1024, inferenceGeo: 'us', messages: [ ['role' => 'user', 'content' => 'Summarize the key points of this document.'], ], ); foreach ($response->content as $block) { if ($block->type === 'text') { echo $block->text, PHP_EOL; } } // Check where inference actually ran echo "Inference geo: {$response->usage->inferenceGeo}\n"; ``` ```ruby Ruby client = Anthropic::Client.new response = client.messages.create( model: "claude-opus-5", max_tokens: 1024, inference_geo: "us", messages: [ {role: "user", content: "Summarize the key points of this document."} ] ) response.content.each do |block| puts block.text if block.type == :text end # Check where inference actually ran puts "Inference geo: #{response.usage.inference_geo}" ``` ### Response The response `usage` object includes an `inference_geo` field indicating where inference ran: ```json Output { "usage": { "input_tokens": 25, "output_tokens": 150, "inference_geo": "us" } } ``` ### Model availability The `inference_geo` parameter is supported on Claude 4.6 and later models. Requests with `inference_geo` on Claude Opus 4.5, Claude Sonnet 4.5, Claude Haiku 4.5, or earlier models return a 400 error. The `inference_geo` parameter is available on the Claude API (first-party) and [Claude Platform on AWS](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws). On Amazon Bedrock and Google Cloud, the inference region is determined by the endpoint URL or inference profile, so `inference_geo` is not applicable. On [Claude in Microsoft Foundry](https://platform.claude.com/docs/en/build-with-claude/claude-in-microsoft-foundry), `inference_geo` is likewise not applicable: deployments hosted on Azure can instead use the US Data Zone Standard deployment type, which keeps inference within the United States. The `inference_geo` parameter is also not available through the [OpenAI SDK compatibility endpoint](https://platform.claude.com/docs/en/cli-sdks-libraries/libraries/openai-sdk). ### Workspace-level restrictions Workspace settings also support restricting which inference geos are available: * **`allowed_inference_geos`:** Restricts which geos a workspace can use. If a request specifies an `inference_geo` not in this list, the API returns an error. * **`default_inference_geo`:** Sets the fallback geo when `inference_geo` is omitted from a request. Individual requests can override this by setting `inference_geo` explicitly. These settings can be configured through the Console or the [Admin API](https://platform.claude.com/docs/en/manage-claude/admin-api) under the `data_residency` field. ## Workspace geo Workspace geo is set when you create a workspace and can't be changed afterward. Currently, `"us"` is the only available workspace geo. To set workspace geo, create a new workspace in the [Console](https://platform.claude.com): 1. Go to **Settings** > **Workspaces**. 2. Create a new workspace. 3. Select the workspace geo. **Claude Platform on AWS:** Workspace geo is not configurable. Claude Managed Agents sessions on this platform run with an effective Workspace geo of `"us"`, which is currently the only available workspace geo. See [Claude Platform on AWS](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws) for data residency considerations specific to that platform. ## Pricing Data residency pricing varies by model generation: * **Claude 4.6 and later models:** US-only inference (`inference_geo: "us"`) is priced at 1.1x the standard rate across all token pricing categories (input tokens, output tokens, cache writes, and cache reads). * **Global routing** (`inference_geo: "global"`): Standard pricing applies. * **Older models:** Don't support `inference_geo` (see [Model availability](https://platform.claude.com/docs/en/manage-claude/data-residency#model-availability)); standard pricing applies. Requests that include the parameter return a 400 error. This pricing applies to the Claude API (first-party) and Claude Platform on AWS. On Claude in Microsoft Foundry, the same 1.1x multiplier applies to deployments hosted on Azure that use the US Data Zone Standard deployment type. Partner-operated platforms (Bedrock and Google Cloud) have their own regional pricing. See [Data residency pricing](https://platform.claude.com/docs/en/about-claude/pricing#data-residency-pricing) for details. The same multiplier applies to [Claude Managed Agents](https://platform.claude.com/docs/en/managed-agents/overview): when an agent's [model configuration](https://platform.claude.com/docs/en/managed-agents/agent-setup) pins `inference_geo` to `"us"`, model requests in sessions running that agent are priced at 1.1x the standard rate. If you have a [Priority Tier](https://platform.claude.com/docs/en/api/service-tiers) commitment, the 1.1x multiplier for US-only inference also affects how tokens are counted against your Priority Tier capacity. Each token consumed with `inference_geo: "us"` draws down 1.1 tokens from your committed TPM, consistent with how other pricing multipliers (such as prompt caching) affect burndown rates. ## Batch API support The `inference_geo` parameter is supported on the [Batch API](https://platform.claude.com/docs/en/build-with-claude/batch-processing). Each request in a batch can specify its own `inference_geo` value. ## Migration from legacy opt-outs If your organization previously opted out of global routing to keep inference in the US, your workspace has been automatically configured with `allowed_inference_geos: ["us"]` and `default_inference_geo: "us"`. No code changes are required. Your existing data residency requirements continue to be enforced through the new geo controls. ### What changed The legacy opt-out was an organization-level setting that restricted all requests to US-based infrastructure. The new data residency controls replace this with two mechanisms: * **Per-request control:** The `inference_geo` parameter lets you specify `"us"` or `"global"` on each API call, giving you request-level flexibility. * **Workspace controls:** The `default_inference_geo` and `allowed_inference_geos` settings in the Console let you enforce geo policies across all keys in a workspace. ### What happened to your workspace Your workspace was migrated automatically: | Legacy setting | New equivalent | | -------------------------------- | --------------------------------------------------------------- | | Global routing opt-out (US only) | `allowed_inference_geos: ["us"]`, `default_inference_geo: "us"` | All API requests using keys from your workspace continue to run on US-based infrastructure. No action is needed to maintain your current behavior. ### If you want to use global routing If your data residency requirements have changed and you want to take advantage of global routing for better performance and availability, update your workspace's inference geo settings to include `"global"` in the allowed geos and set `default_inference_geo` to `"global"`. See [Workspace-level restrictions](https://platform.claude.com/docs/en/manage-claude/data-residency#workspace-level-restrictions) for details. ### Pricing impact Legacy models are unaffected by this migration. For current pricing on newer models, see [Pricing](https://platform.claude.com/docs/en/manage-claude/data-residency#pricing). ## Current limitations * **Shared rate limits:** Rate limits are shared across all geos. * **Inference geo:** Only `"us"` and `"global"` are available. * **Workspace geo:** Only `"us"` is currently available. Workspace geo can't be changed after workspace creation. ## Next steps View data residency pricing details. Learn about workspace configuration. Track usage and costs by data residency. ### Data & compliance > Encryption keys --- title: Configure AWS KMS for CMEK url: https://platform.claude.com/docs/en/manage-claude/cmek-aws-kms description: Use AWS KMS to provide an encryption key for your organization. --- ```bash Configure with the /claude-api skill in Claude Code claude "/claude-api help me configure a customer-managed encryption key with AWS KMS" ``` This guide walks through configuring an [AWS KMS](https://aws.amazon.com/kms/) key as a [customer-managed encryption key (CMEK)](https://platform.claude.com/docs/en/manage-claude/cmek) for your Anthropic organization. Enabling CMEK is permanent. If your KMS key is deleted or disabled, Anthropic cannot recover the data encrypted under it. Review the [warnings and limitations](https://platform.claude.com/docs/en/manage-claude/cmek) before you begin. ## Prerequisites * An AWS account with permissions to create KMS keys and set key policies (`kms:CreateKey` and `kms:PutKeyPolicy`). * An Anthropic Admin API key for your organization. * The [AWS CLI](https://aws.amazon.com/cli/) installed and authenticated. ## Amazon Resource Name (ARN) for Anthropic To have Anthropic use your encryption key, you must give Anthropic's IAM role a KMS key it can use for encrypting data. The ARN for Anthropic CMEK is: ```text wrap arn:aws:iam::915198916910:role/anthropic-cmek-client-us ``` Use only this published ARN. Never trust an identifier provided over email, chat, or any onboarding channel. ## Encryption key setup The key policy grants Anthropic's IAM role cross-account access. Three statements are required: 1. **Account root admin:** the standard KMS pattern. Your account retains full admin control. 2. **Anthropic encrypt and decrypt:** the `kms:Encrypt` and `kms:Decrypt` actions, which Anthropic uses to encrypt and decrypt the data keys that protect your workspace data (envelope encryption). 3. **Anthropic describe:** the metadata read Anthropic performs at startup. It is granted separately because `DescribeKey` has no `EncryptionContext` parameter, so an `EncryptionContext` condition on this action would always deny. ```bash export YOUR_ACCOUNT=$(aws sts get-caller-identity --query Account --output text) aws kms create-key \ --region \ --description "Anthropic CMEK" \ --key-usage ENCRYPT_DECRYPT \ --policy "{ \"Version\": \"2012-10-17\", \"Statement\": [ { \"Sid\": \"AccountRootAdmin\", \"Effect\": \"Allow\", \"Principal\": {\"AWS\": \"arn:aws:iam::${YOUR_ACCOUNT}:root\"}, \"Action\": \"kms:*\", \"Resource\": \"*\" }, { \"Sid\": \"AllowAnthropicCMEKCrypto\", \"Effect\": \"Allow\", \"Principal\": {\"AWS\": \"arn:aws:iam::915198916910:role/anthropic-cmek-client-us\"}, \"Action\": [\"kms:Encrypt\", \"kms:Decrypt\"], \"Resource\": \"*\", \"Condition\": { \"StringEquals\": { \"kms:EncryptionContext:anthropic:compartment_uuid\": [ \"00000000-0000-0000-0000-000000000000\", \"\" ] } } }, { \"Sid\": \"AllowAnthropicCMEKDescribe\", \"Effect\": \"Allow\", \"Principal\": {\"AWS\": \"arn:aws:iam::915198916910:role/anthropic-cmek-client-us\"}, \"Action\": \"kms:DescribeKey\", \"Resource\": \"*\" } ] }" ``` Capture `KeyMetadata.Arn` from the output. You need it when you register the key in the next step. The `EncryptionContext` condition is recommended but optional. Anthropic always includes your workspace's compartment ID in the encryption context, so ciphertext is cryptographically bound to that compartment regardless. Adding the condition provides defense-in-depth at the IAM layer. To start without it, omit the `Condition` block from the `AllowAnthropicCMEKCrypto` statement and add it later with `kms:PutKeyPolicy`. **Finding your compartment ID:** Where to find your compartment ID differs between Claude Platform and Claude Enterprise. See the **Claude Platform** and **Claude Enterprise** tabs under **Register the key with Anthropic**. You can also create the key from the AWS Console. Choose a symmetric key with the encrypt and decrypt key usage, a single-region key, and KMS key material origin. The Create-key wizard commits a key policy at its **Review** step: If you add Anthropic's account ID `915198916910` under key usage permissions there, the generated policy grants the whole Anthropic account broader actions (such as `kms:ReEncrypt*` and `kms:GenerateDataKey*`) with no `EncryptionContext` condition, and validation would still succeed against it. To avoid leaving an over-permissive key, finish the wizard with administrative permissions only, then open the key's **Key policy** tab and replace the JSON with the role-scoped policy shown earlier (the three statements scoped to the `anthropic-cmek-client-us` role, with the `EncryptionContext` condition). ![AWS KMS Create key wizard on the Configure key step, with Symmetric key type, Encrypt and decrypt key usage, and Single-Region key selected.](https://platform.claude.com/docs/images/cmek/aws-configure-key.png) ![AWS KMS Add labels step with an alias of anthropic-cmek and a description of Anthropic CMEK.](https://platform.claude.com/docs/images/cmek/aws-add-labels.png) ![AWS KMS Define key administrative permissions step listing IAM roles that can administer the key.](https://platform.claude.com/docs/images/cmek/aws-admin-permissions.png) ![AWS KMS Define key usage permissions step with Anthropic's account ID entered under Other AWS accounts.](https://platform.claude.com/docs/images/cmek/aws-usage-permissions.png) ## Register the key with Anthropic How you register the key depends on which product you use. **Finding your compartment ID:** Each workspace has a compartment ID that scopes its CMEK data. Find it in the Claude Console under **Workspace > Security > Encryption keys** (the **Compartment ID** field), or read the `compartment_id` field returned by the [Get Workspace](https://platform.claude.com/docs/en/api/admin-api/workspaces/get-workspace) endpoint. Substitute that value for `` in the preceding key policy. Key validation always sends the all-zeros compartment UUID (`00000000-0000-0000-0000-000000000000`) as the encryption context, because validation runs before the key is attached to any workspace. Live traffic sends the compartment ID of each attached workspace. Any `EncryptionContext` condition must allow the all-zeros value plus the compartment ID of every workspace the key is attached to. Validation also runs again whenever key setup is re-run, so keep the all-zeros entry in place permanently. To attach the key to an additional workspace, add that workspace's compartment ID to the condition with `kms:PutKeyPolicy` before attaching. Create an external key configuration through the Admin API. For organizations on [Claude Platform on AWS](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws), the external key endpoints are not yet available. Register, validate, and attach your key in the Claude Console instead. ```bash curl -sS https://api.anthropic.com/v1/organizations/external_keys \ -H "x-api-key: " \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "display_name": "", "geo": "us", "provider_config": { "type": "aws", "kms_arn": "", "role_arn": "arn:aws:iam::915198916910:role/anthropic-cmek-client-us" } }' ``` The response contains the external key ID: ```json { "type": "external_key", "id": "ekey_", "display_name": "" } ``` Trigger an encrypt and decrypt round-trip against your key. ```bash curl -sS -X POST https://api.anthropic.com/v1/organizations/external_keys/ekey_/validate \ -H "x-api-key: " \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{}' ``` A successful response looks like this: ```json { "type": "external_key_validation", "status": "success", "error": null } ``` If validation fails, common causes are: * **Encryption context mismatch:** Validation fails while data traffic works (or the reverse) with an opaque `AccessDeniedException` when a `kms:EncryptionContext:anthropic:compartment_uuid` condition allows only one of the two values Anthropic sends. Validation sends the all-zeros UUID (`00000000-0000-0000-0000-000000000000`); live traffic sends the attached workspace's compartment ID. Confirm the condition lists both. To rule the condition out entirely, temporarily remove the `Condition` block from the `AllowAnthropicCMEKCrypto` statement and re-validate. * **Resource control policies (RCPs):** If your AWS organization has an RCP that denies KMS operations when `aws:PrincipalOrgID` does not match your org, it blocks Anthropic's cross-account role. The RCP needs a carve-out for this key or for Anthropic's role ARN. Service control policies do not apply here, because they do not evaluate for external principals calling through resource-based policies. * **Access granted through IAM instead of the key policy:** Cross-account KMS access must be granted in the key policy itself, not through an IAM policy in your account. Check with `aws kms get-key-policy --key-id --policy-name default`. * **Region mismatch:** Confirm the key's region is one Anthropic operates in for the geo tier you configured. ```bash curl -sS -X POST https://api.anthropic.com/v1/organizations/workspaces/ \ -H "x-api-key: " \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "external_key_id": "ekey_" }' ``` In [claude.ai > Organization settings > Data and privacy](https://claude.ai/admin-settings/data-privacy-controls), open **Encryption keys**, then click **Add key**. Choose **AWS** and click **Continue**, then paste the Key ARN from the previous step and click **Add**. Anthropic validates the key with an encrypt and decrypt round-trip. Once it shows as verified, your organization is CMEK-protected from that point forward. The key details step of this flow displays your organization's **Compartment ID** with a copy button. Substitute that value for `` in the key policy (see the Create the KMS key step under Encryption key setup); you can open the flow to copy the ID before you create the key. After setup, the ID remains visible on the key under **Encryption keys**. On Claude Enterprise, CMEK applies to the whole organization, so there is no separate workspace attach step, and an organization can have only one key. ## Terraform For infrastructure-as-code deployments, the same steps map to the `aws` provider with the `aws_kms_key` and `aws_kms_alias` resources. --- title: Configure Azure Key Vault for CMEK url: https://platform.claude.com/docs/en/manage-claude/cmek-azure-key-vault description: Use Azure Key Vault to provide an encryption key for your organization. --- ```bash Configure with the /claude-api skill in Claude Code claude "/claude-api help me configure a customer-managed encryption key with Azure Key Vault" ``` This guide walks through configuring an Azure Key Vault key as a [customer-managed encryption key (CMEK)](https://platform.claude.com/docs/en/manage-claude/cmek) for your Anthropic organization. Enabling CMEK is permanent. If your Key Vault key is deleted or disabled, Anthropic cannot recover the data encrypted under it. Review the [warnings and limitations](https://platform.claude.com/docs/en/manage-claude/cmek) before you start. ## Prerequisites * An Azure Key Vault with **RBAC authorization enabled** (`enableRbacAuthorization: true`) and **public network access allowed**. Anthropic calls your vault over the public data-plane endpoint; private endpoints are not supported. * **Purge protection enabled** (`enablePurgeProtection: true`) on the vault. Without it, a deleted key can be permanently purged during the soft-delete retention window, causing irreversible loss of your CMEK-protected data. Purge protection cannot be disabled once enabled. * Permissions to create keys in the vault and to assign RBAC roles on it. * Permissions to create service principals in your Entra tenant (`Application Administrator`, `Cloud Application Administrator`, or an equivalent custom role). * An Anthropic Admin API key for your organization. * The [`az` CLI](https://learn.microsoft.com/en-us/cli/azure/?view=azure-cli-latest) installed and authenticated. * **Diagnostic Settings** configured on the vault to route the `AuditEvent` log category to Log Analytics, a storage account, or an event hub. Azure Key Vault does not emit data-plane audit logs (such as `KeyWrap`, `KeyUnwrap`, and `KeyGet`) by default, so without this you get no audit trail for Anthropic's key operations. ## Anthropic app information To have Anthropic use your encryption key, you must configure an Anthropic multitenant application ID and display name. Those values are: | Field | Value | | ------------------------------ | -------------------------------------- | | Multitenant app client ID (US) | `8635ae1a-3e5d-44e8-a4ed-e0f614466f87` | | App display name | `anthropic-cmek-client-us` | Use only this published client ID and display name. Never trust an identifier provided over email, chat, or any onboarding channel. ## Encryption key setup This creates a service principal in your Entra tenant for Anthropic's CMEK client application. The application requests no Microsoft Graph permissions; it exists solely as a federation target for Key Vault data-plane access. ```bash az ad sp create --id 8635ae1a-3e5d-44e8-a4ed-e0f614466f87 ``` From the output, capture the `id` field. This is the service principal's object ID in your tenant, which you use when you assign the RBAC role. ```json { "appId": "8635ae1a-3e5d-44e8-a4ed-e0f614466f87", "displayName": "anthropic-cmek-client-us", "id": "" } ``` If the service principal already exists in your tenant (from a prior attempt or another integration), `az ad sp create` exits with an "already exists" error. Fetch its object ID instead: ```bash az ad sp show --id 8635ae1a-3e5d-44e8-a4ed-e0f614466f87 --query id -o tsv ``` This step has no Portal equivalent. If you do not have the Azure CLI installed locally, open Cloud Shell from the Portal's top navigation bar. After the command succeeds, you can find the service principal's object ID in **Microsoft Entra ID > Enterprise applications** by clearing the default application-type filter and searching for `anthropic-cmek-client-us`. ![Microsoft Entra enterprise application overview for anthropic-cmek-client-us, showing its Application ID and Object ID.](https://platform.claude.com/docs/images/cmek/azure-service-principal.png) Azure Key Vault does not support symmetric key wrapping, so the key must be RSA (3072-bit or larger) with `wrapKey` and `unwrapKey` in its allowed operations. ```bash az keyvault key create \ --vault-name \ --name \ --kty RSA --size 3072 \ --ops wrapKey unwrapKey ``` For HSM-backed keys, use `--kty RSA-HSM` (requires a Premium-SKU vault). Software-protected RSA keys are acceptable for this integration. From the Portal, open your Key Vault, select **Keys**, then **Generate/Import**. Set the key type to RSA and the size to 3072 or larger. To restrict the key to wrap and unwrap only, open the key version, scroll to **Permitted operations**, and uncheck everything except **Wrap Key** and **Unwrap Key**. ![Azure Key Vault Create a key page with the Generate option, RSA key type, and 3072 RSA key size selected.](https://platform.claude.com/docs/images/cmek/azure-create-key.png) ![Azure Key Vault key version with Permitted operations limited to Wrap Key and Unwrap Key.](https://platform.claude.com/docs/images/cmek/azure-permitted-operations.png) Assign the `Key Vault Crypto User` role to the service principal from the first step, scoped to the **individual key** rather than the whole vault. ```bash VAULT_ID=$(az keyvault show --name --query id -o tsv) az role assignment create \ --role "Key Vault Crypto User" \ --assignee-object-id \ --assignee-principal-type ServicePrincipal \ --scope "${VAULT_ID}/keys/" ``` The built-in `Key Vault Crypto User` role grants key cryptographic operations (encrypt, decrypt, wrap, unwrap, sign, verify) plus key read on its assigned scope. The `--ops wrapKey unwrapKey` restriction you set on the key in the previous step further narrows which of those operations can succeed against this key, so in practice Anthropic can only wrap and unwrap. From the Portal, open the **key** (not the vault), select its **Access control (IAM)** tab, click **Add > Add role assignment**, select **Key Vault Crypto User**, and assign it to the `anthropic-cmek-client-us` service principal. **Dedicated vault alternative:** Microsoft recommends a dedicated vault per application with roles assigned at the vault scope. If you provision a vault that holds only this Anthropic CMEK key, you can assign the role at the vault scope instead and the effect is identical. Scope to the individual key when the key lives in a shared vault. ![Key Vault IAM role assignments showing anthropic-cmek-client-us assigned the Key Vault Crypto User role.](https://platform.claude.com/docs/images/cmek/azure-role-assignment.png) ```bash az keyvault show --name \ --query "{rbac:properties.enableRbacAuthorization, purge:properties.enablePurgeProtection, pub:properties.publicNetworkAccess, net:properties.networkAcls.defaultAction, ipRules:properties.networkAcls.ipRules, uri:properties.vaultUri, tenantId:properties.tenantId}" ``` Confirm that: * `rbac` is `true`. * `purge` is `true`. If it is `false` or `null`, enable purge protection on the vault before proceeding. Without it, a soft-deleted key can be permanently purged during the retention window, making your CMEK-protected data unrecoverable. * `pub` is `"Enabled"`. If it is `"Disabled"`, Anthropic cannot reach the vault over its public data-plane endpoint and validation fails. * `net` is `"Allow"`, or, if it is `"Deny"`, that `ipRules` include Anthropic's egress ranges (contact Anthropic for the current list). * `uri` is the vault URI you use when you register the key. * `tenantId` is the tenant that governs the vault. Use this value as `tenant_id` when you register the key, not the tenant of your currently-active subscription (the two can differ in cross-tenant setups). ## Register the key with Anthropic How you register the key depends on which product you use. Create an external key configuration through the Admin API. ```bash curl -sS https://api.anthropic.com/v1/organizations/external_keys \ -H "x-api-key: " \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "display_name": "", "geo": "us", "provider_config": { "type": "azure", "vault_uri": "https://.vault.azure.net/", "key_name": "", "tenant_id": "" } }' ``` The response contains the external key ID: ```json { "type": "external_key", "id": "ekey_", "display_name": "" } ``` Trigger an encrypt and decrypt round-trip against your key. This confirms that Anthropic can authenticate to your tenant and perform wrap and unwrap operations. ```bash curl -sS -X POST https://api.anthropic.com/v1/organizations/external_keys/ekey_/validate \ -H "x-api-key: " \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" -d '{}' ``` A successful response looks like this: ```json { "type": "external_key_validation", "status": "success", "error": null } ``` If validation fails, the `error` field describes the problem. Common causes are: * **RBAC propagation delay:** role assignments can take a few minutes to take effect. Wait and retry. * **Network ACLs blocking Anthropic:** confirm public network access and `ipRules` as described in the verification step. * **Conditional access policies on workload identities:** if your tenant has conditional access policies that target service principals, exclude the Anthropic service principal or add Anthropic's egress ranges to the policy's named locations. Once the key is validated, attach it to a workspace to enable CMEK for that workspace's data. ```bash curl -sS -X POST https://api.anthropic.com/v1/organizations/workspaces/ \ -H "x-api-key: " \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "external_key_id": "ekey_" }' ``` In [claude.ai > Organization settings > Data and privacy](https://claude.ai/admin-settings/data-privacy-controls), open **Encryption keys**, then click **Add key**. Choose **Azure**, enter the vault URI, key name, and tenant ID from the verification step, and click **Continue**. Anthropic validates the key with an encrypt and decrypt round-trip. Once it shows as verified, your organization is CMEK-protected from that point forward. On Claude Enterprise, CMEK applies to the whole organization, so there is no separate workspace attach step, and an organization can have only one key. ## Terraform For infrastructure-as-code deployments, the same steps map to the `azurerm` and `azuread` providers. --- title: Configure Google Cloud KMS for CMEK url: https://platform.claude.com/docs/en/manage-claude/cmek-google-cloud-kms description: Use Google Cloud KMS to provide an encryption key for your organization. --- ```bash Configure with the /claude-api skill in Claude Code claude "/claude-api help me configure a customer-managed encryption key with Google Cloud KMS" ``` This guide walks through configuring a Google Cloud KMS key as a [customer-managed encryption key (CMEK)](https://platform.claude.com/docs/en/manage-claude/cmek) for your Anthropic organization. Enabling CMEK is permanent. If your KMS key is deleted or disabled, Anthropic cannot recover the data encrypted under it. Review the [warnings and limitations](https://platform.claude.com/docs/en/manage-claude/cmek) before you begin. ## Prerequisites * A Google Cloud project with billing enabled. * The Cloud KMS API enabled (`cloudkms.googleapis.com`). * Permissions to create KMS key rings and keys, and to set IAM policy on them (`roles/cloudkms.admin` or equivalent). * An Anthropic Admin API key for your organization. * The [`gcloud` CLI](https://cloud.google.com/cli) installed and authenticated. * Cloud KMS **Data Access audit logs** enabled for the project (IAM & Admin > Audit Logs > Cloud Key Management Service, with `DATA_READ` and `DATA_WRITE`). These are off by default; without them, Anthropic's encrypt and decrypt operations produce no entries in Cloud Logging. ## Anthropic service account email To have Anthropic use your encryption key, you must give Anthropic's service account a key it can use for encrypting data. The service account email for Anthropic CMEK is: ```text wrap anthropic-cmek-client-us@gcp-anthropic-cmek-clients.iam.gserviceaccount.com ``` Use only this published service account email. Never trust an identifier provided over email, chat, or any onboarding channel. **Domain restricted sharing:** If your project is under a Google Cloud organization that enforces `constraints/iam.allowedPolicyMemberDomains`, the following IAM bindings are rejected because the Anthropic service account is outside your organization. You need either a project-level carve-out on that constraint, or to add Anthropic's Cloud Identity customer ID (format `C0xxxxxxxx`) to the allowed list. Contact Anthropic for the customer ID if needed. ## Encryption key setup Skip this step if you already have a key ring to reuse. Key rings are regional. Choose a single-region US location such as `us-east5` that matches the Anthropic geography you are configuring. Multi-region locations like `us` and `global` are not supported. ```bash gcloud kms keyrings create \ --project= \ --location= ``` Create a symmetric key with the `ENCRYPT_DECRYPT` purpose. Anthropic strongly recommends HSM protection: Cloud KMS HSM keys are FIPS 140-2 Level 3 validated, and the cost delta over software keys is small. ```bash gcloud kms keys create \ --project= \ --location= \ --keyring= \ --purpose=encryption \ --protection-level=hsm ``` For software protection instead, omit `--protection-level=hsm`. Nothing else in this guide changes. You can also create the key from the Google Cloud Console. Open the key ring, click **Create key**, select **Generated key**, set the purpose and algorithm to symmetric encrypt and decrypt, and choose **HSM** under protection level. ![Google Cloud KMS Create key page with HSM protection level and a Symmetric encrypt/decrypt purpose.](https://platform.claude.com/docs/images/cmek/gcp-create-key.png) Two key-level IAM bindings are required. Both are scoped to the single crypto key, not project-wide or keyring-wide. Encrypt and decrypt, which Anthropic uses to encrypt and decrypt the data keys that protect your workspace data (envelope encryption): ```bash gcloud kms keys add-iam-policy-binding \ --project= \ --location= \ --keyring= \ --member="serviceAccount:anthropic-cmek-client-us@gcp-anthropic-cmek-clients.iam.gserviceaccount.com" \ --role=roles/cloudkms.cryptoKeyEncrypterDecrypter ``` Viewer, for the metadata read (`cryptoKeys.get`) Anthropic performs at startup to validate the key's purpose and algorithm: ```bash gcloud kms keys add-iam-policy-binding \ --project= \ --location= \ --keyring= \ --member="serviceAccount:anthropic-cmek-client-us@gcp-anthropic-cmek-clients.iam.gserviceaccount.com" \ --role=roles/cloudkms.viewer ``` From the Console, select the key, open the **Permissions** panel, click **Grant access**, and add the service account with both the Cloud KMS CryptoKey Encrypter/Decrypter and Cloud KMS Viewer roles. Make sure you are on the key's permissions page, not the key ring or project, so the grant is scoped to this key only. ![Grant access dialog with the Anthropic service account assigned Cloud KMS CryptoKey Encrypter/Decrypter and Viewer roles.](https://platform.claude.com/docs/images/cmek/gcp-grant-access.png) You pass this to Anthropic when you register the key. The format is: ```text wrap projects//locations//keyRings//cryptoKeys/ ``` Retrieve it with: ```bash gcloud kms keys describe \ --project= \ --location= \ --keyring= \ --format="value(name)" ``` From the Console, open the key's details page and click **Copy resource name**. ![Google Cloud key ring details with the Copy resource name action highlighted in the key's actions menu.](https://platform.claude.com/docs/images/cmek/gcp-copy-resource-name.png) ## Register the key with Anthropic How you register the key depends on which product you use. Create an external key configuration through the Admin API, using the resource name from the Note the full key resource name step under Encryption key setup. ```bash curl -sS https://api.anthropic.com/v1/organizations/external_keys \ -H "x-api-key: " \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "display_name": "", "geo": "us", "provider_config": { "type": "gcp", "key_name": "projects//locations//keyRings//cryptoKeys/" } }' ``` The response contains the external key ID: ```json { "type": "external_key", "id": "ekey_", "display_name": "" } ``` Trigger an encrypt and decrypt round-trip against your key. ```bash curl -sS -X POST https://api.anthropic.com/v1/organizations/external_keys/ekey_/validate \ -H "x-api-key: " \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{}' ``` A successful response looks like this: ```json { "type": "external_key_validation", "status": "success", "error": null } ``` If validation fails, common causes are: * **VPC Service Controls:** if a service perimeter protects Cloud KMS in your project, add Anthropic to an access level on the perimeter (or exclude the key's project) so Anthropic can reach the key. * **Domain restricted sharing:** the `constraints/iam.allowedPolicyMemberDomains` org policy can strip the Anthropic service account binding (see the earlier note). Confirm the binding is present with `gcloud kms keys get-iam-policy --project= --location= --keyring=`. * **Disabled or destroyed key version:** confirm the key's primary version is enabled, and not disabled, scheduled for destruction, or destroyed. ```bash curl -sS -X POST https://api.anthropic.com/v1/organizations/workspaces/ \ -H "x-api-key: " \ -H "anthropic-version: 2023-06-01" \ -H "content-type: application/json" \ -d '{ "external_key_id": "ekey_" }' ``` In [claude.ai > Organization settings > Data and privacy](https://claude.ai/admin-settings/data-privacy-controls), open **Encryption keys**, then click **Add key**. Choose **Google Cloud**, paste the full key resource name from the previous step, and click **Continue**. Anthropic validates the key with an encrypt and decrypt round-trip. Once it shows as verified, your organization is CMEK-protected from that point forward. On Claude Enterprise, CMEK applies to the whole organization, so there is no separate workspace attach step, and an organization can have only one key. ## Terraform For infrastructure-as-code deployments, the same steps map to the `google` provider with the `google_kms_key_ring`, `google_kms_crypto_key`, and `google_kms_crypto_key_iam_member` resources. --- title: Customer-managed encryption keys url: https://platform.claude.com/docs/en/manage-claude/cmek description: Encrypt Claude workspace data at rest with a key you control. --- ```bash Learn more with the /claude-api skill in Claude Code claude "/claude-api tell me about customer-managed encryption keys" ``` A customer-managed encryption key (CMEK) lets you provision an encryption key in your own [AWS KMS](https://aws.amazon.com/kms/), [Google Cloud KMS](https://cloud.google.com/security/products/security-key-management), or [Azure Key Vault](https://azure.microsoft.com/en-us/products/key-vault) and have Anthropic use it to encrypt certain workspace data at rest. You retain full control of the key, including rotation, audit, and revocation, and the key operations Anthropic performs against your key are recorded in your cloud provider's audit logs. The use of CMEK is optional. Eligible organizations can **opt in** to use customer-managed encryption keys instead of the default encryption that Anthropic provides. To activate CMEK, contact your Anthropic account team. **Enabling CMEK is permanent and can cause irreversible data loss** Enabling CMEK is permanent. Anthropic keeps no copy of your key, so misconfiguration or key loss can permanently destroy your CMEK-protected data. If you are uncertain about any step, contact your Anthropic representative before applying changes. * **Permanent data loss:** If your encryption key is deleted, scheduled for deletion, or has its key material destroyed, Anthropic cannot recover your data. * **Identifier verification is mandatory:** Granting key access to an incorrect or spoofed principal can expose your data to an unauthorized party. Always verify the Anthropic identifier against the published production identities in each configuration guide. Never trust an identifier provided over email, chat, or any onboarding channel. ## How it works Only Organization Admins (on Claude Platform) or Owners and the Primary Owner (on Claude Enterprise) can configure CMEK. On Claude Platform, CMEK is scoped per workspace and configured with the Admin API. On Claude Enterprise, CMEK is scoped per organization and configured in [claude.ai > Organization settings > Data and privacy](https://claude.ai/admin-settings/data-privacy-controls). On either product, CMEK protects data written after the key is enabled. Existing data (prior chats, files, and sessions) remains encrypted with Anthropic-managed keys and is not re-encrypted under your key. CMEK configuration events appear in the [Compliance API Activity Feed](https://platform.claude.com/docs/en/manage-claude/compliance-activity-feed). The key operations Anthropic performs against your key (such as wrapping and unwrapping data keys) do not appear in the Compliance API; they appear in your cloud provider's audit logs. Anthropic calls your key management service from its standard public IP range. If you restrict access to your key management service by IP, allow the addresses listed in [IP addresses](https://platform.claude.com/docs/en/api/ip-addresses). ## Prerequisites * Permissions to create encryption keys and manage key access in the account, project, or subscription that will host the encryption key. * An Organization Admin role in the Claude Console on Claude Platform, or an Owner or Primary Owner role on Claude Enterprise. * Data retention configuration: CMEK is allowed with [Zero data retention (ZDR)](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention) for both Claude Platform and Claude Enterprise. ## Availability and regions CMEK is currently available in US regions only, and all encryption operations are processed in US regions. On [Claude Platform on AWS](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws), CMEK is available with AWS KMS keys only; Google Cloud KMS and Azure Key Vault keys cannot be registered. Create, validate, and attach keys in the Claude Console; the `external_keys` API endpoints are not currently available on Claude Platform on AWS. The key must be in the same AWS region as the workspace it is attached to. For minimal latency, choose a region close to Anthropic's US infrastructure: | Provider | Recommended regions | | ------------ | --------------------------- | | AWS | `us-east-2` | | Google Cloud | `us-central1`, `us-east5` | | Azure | `northcentralus`, `eastus2` | ## What CMEK protects What CMEK covers depends on which product you use. ### Encrypted **Claude Platform** * Message content, files and attachments (both inline attachments sent with a request and Files API uploads), and MCP and tool configuration. * [Claude Managed Agents](https://platform.claude.com/docs/en/managed-agents/overview) data, including agent configurations, environments, webhooks, and sessions and their events. **Claude Enterprise** * Chat content, including skills, plugins, and artifacts. * Chat attachments and project attachments. * Claude Code on the CLI, including message content. * Cowork in Claude Desktop. * Office agents. * Claude in Chrome. On both products, backups and snapshots inherit the key. ### Disabled or modified Some features are turned off or substantially modified when CMEK is enabled. This list is not exhaustive; review it with your team before enabling CMEK. **Claude Platform** * Workbench in the Claude Console is disabled. * Portions of the Compliance API that return raw content, such as prompts, responses, and files, are disabled. * Other beta and research preview features may not be covered by CMEK. **Claude Enterprise** * Conversation history search is disabled. Conversation titles are encrypted, so searching by title or content returns no results. * Search across large numbers of files is slower. * Certain analytics are degraded: admin analytics for claude.ai skills and connectors (under claude.ai/analytics/usage and through the [Claude Enterprise Analytics API](https://platform.claude.com/docs/en/manage-claude/analytics-api)), Claude smart reports (under claude.ai/analytics/insights), and Claude Code contribution metrics (under claude.ai/analytics/claude-code). * Audit log exports are disabled. * Signed URLs for temporary file exchanges are disabled. These back organization data exports in claude.ai and Claude Code Remote file flows such as screenshot updates. * Personal preferences are disabled for users who belong to a CMEK-protected organization, across all organizations under the same parent. Users who do not belong to a CMEK-protected organization can still use them across all organizations. * Compliance API [local session transcripts](https://platform.claude.com/docs/en/manage-claude/compliance-sessions#retrieve-local-sessions) (Cowork and Claude Code on users' machines) currently return no message content. Session metadata is listed as usual, and the local session messages endpoint (`GET /v1/compliance/apps/sessions/local/{session_id}/messages`) returns each message with its content marked unavailable; see [Retrieve a local session transcript](https://platform.claude.com/docs/en/manage-claude/compliance-sessions#retrieve-a-local-session-transcript) for the response shape. ### Not encrypted These features remain available, but their data is not encrypted under your key. You can disable any feature that is not appropriate for your use case in **Settings**. **Claude Platform** * Data that is not at rest (such as cache) and data with a TTL shorter than 24 hours. * Activity Feed, audit logs, and telemetry network traffic such as OTEL, so customers can maintain compliance even if a key is revoked. * Claude Managed Agents [vault credential](https://platform.claude.com/docs/en/managed-agents/vaults) values, such as OAuth tokens and client secrets. These are stored under Anthropic-managed encryption, are write-only, and are never returned in API responses. * [User profiles](https://platform.claude.com/docs/en/api/beta/user_profiles): the `name`, `external_id`, and `metadata` fields are stored under Anthropic-managed encryption, not your key. Do not store sensitive personal data in profile `metadata`. **Claude Enterprise** * Claude Code Desktop, Claude Code on the web, and Claude in Slack. Anthropic recommends disabling any of these that are not appropriate for your use case in the admin console. * Beta and research preview features may not be covered by CMEK and can break in CMEK organizations, for example, Claude Security and Claude Design. * On-demand data export under **Settings** > **Privacy**. On both products, account data for users in your organization (such as names, email addresses, and profile pictures) is not encrypted under your key. ### Feature support The following Claude Platform APIs and tools store data at rest under your key when CMEK is enabled: | APIs | Tools and features | | --------------------- | --------------------------------------------------------------------------------------------------- | | Messages | Web search | | Models | Web fetch | | Files | Code execution | | Batch | Bash tool | | Skills | Text editor tool | | Claude Managed Agents | MCP connector | | | Structured outputs (not available for Claude Fable 5 or Claude Mythos models in CMEK organizations) | | | Advisor tool | | | Computer use | | | Context management | ## Limited preservation outside your key In three narrow cases, Anthropic may preserve specific records under Anthropic-managed encryption: * Where Anthropic is required by law to retain records (for example, material reported to NCMEC under 18 U.S.C. § 2258A). * Exigent risk of serious harm (for example, CBRNE weapons development, offensive cyberattacks, or imminent threats of violence). * Violations of Section D.4 of Anthropic's [Commercial Terms of Service](https://www.anthropic.com/legal/commercial-terms) or equivalent terms in a customer's other applicable agreement with Anthropic. Outside of [CSAM screening](https://support.claude.com/en/articles/9020328-csam-detection-and-reporting), preservation requires a human reviewer's explicit decision and follows Anthropic's [retention policy for commercial data](https://privacy.claude.com/en/articles/10023548-how-long-do-you-store-my-data). For every instance of preservation, a corresponding [Compliance API Activity Feed](https://platform.claude.com/docs/en/manage-claude/compliance-activity-feed) event is generated with a reason code conveying the purpose of the preservation. See [CMEK content preservation](https://platform.claude.com/docs/en/manage-claude/access-transparency#cmek-content-preservation) for details. Safety screening metadata (records derived from Anthropic's automated safety scans, such as pattern identifiers and match indicators, not conversation content) is retained under Anthropic-managed encryption and remains readable after key revocation. ## Limitations * **Irreversible action:** Once a key is attached to a workspace, it cannot be detached or swapped. On Claude Platform, attaching a key also locks the workspace's data retention setting: you cannot turn off 30-day data retention for that workspace, and returning to zero data retention requires creating a new workspace and moving your traffic to it. Rotating the key material within the same key (for example, AWS KMS automatic rotation, a Cloud KMS rotation schedule, or an Azure Key Vault rotation policy) is supported transparently and requires no change in Anthropic. Switching to a *different* key requires creating a new workspace with the new key and migrating your data. Revoking or disabling the key makes all CMEK-protected data in that workspace permanently inaccessible, with no backout path. * **No retroactive encryption:** CMEK only protects data written after the key is enabled. * **Latency:** Operations that wrap or unwrap data keys make a round-trip to your key management service, which can add a small amount of latency to actions that read or write data at rest. * **Revocation delay:** Key revocation can take up to 1 hour (the cache TTL). Requests already in flight during that window may continue to succeed. * **KMS costs:** CMEK requires a key in a third-party key management service (AWS KMS, Google Cloud KMS, or Azure Key Vault), which may incur separate charges billed by your KMS provider. ## Configure your provider Follow the guide for the key management service you use. Create an AWS KMS key with a cross-account key policy, then register and validate it. Create a Cloud KMS crypto key, grant Anthropic's service account access, then register it. Create an RSA key, grant the Anthropic service principal access, then register and validate it. ### Data & compliance > Inference hooks --- title: Configure Inference hooks url: https://platform.claude.com/docs/en/manage-claude/inference-hooks-configuration description: Allow Inference hooks for your Claude Enterprise organization, connect your AI security server, and control enforcement, failure handling, and rollout. --- Inference hooks are in beta and available to Claude Enterprise organizations. Configuring them requires the `organization:manage` permission, which the built-in Admin, Owner, and Primary owner roles hold, as does any custom role granted it. Inference hooks send prompts from your organization to an AI security server you choose, and hold each request for an allow or deny verdict before Claude processes it. This page walks through turning the feature on, connecting your server, and controlling enforcement. For what Inference hooks are and when to use them, see the [Inference hooks overview](https://platform.claude.com/docs/en/manage-claude/inference-hooks). For building the AI security server itself, see [Develop an Inference hooks integration](https://platform.claude.com/docs/en/manage-claude/inference-hooks-endpoint). ## Before you begin You need: * The `organization:manage` permission in claude.ai. The built-in **Admin**, **Owner**, and **Primary owner** roles hold it, as does any custom role it has been granted. * An AI security server HTTPS endpoint that accepts verdict requests: an `https://` URL on port 443, on a publicly routable host, reachable without redirects. Reverse-tunnel hosts (ngrok and similar tunnel services) are not supported: Anthropic's network policy blocks them. Don't test through a tunnel; host your server on a domain you control. For the full [hosting requirements](https://platform.claude.com/docs/en/manage-claude/inference-hooks-endpoint#receive-a-request), and to build the server and verify signed requests, see [Develop an Inference hooks integration](https://platform.claude.com/docs/en/manage-claude/inference-hooks-endpoint). ## Set up Inference hooks There are three enforcement states: **off** (**Enforce verdicts** is off: your AI security server is never contacted and prompts are not inspected), **shadow** (**Enforce verdicts** is on with **Mode** set to **Shadow mode**: your AI security server receives prompts and returns verdicts, and nothing is blocked), and **enforcing** (**Enforce verdicts** is on with **Mode** set to **Allow the request** or **Block the request**: a deny blocks the request). The following steps take a new configuration from off to enforcing. Go to claude.ai > **Organization settings** > **Data and privacy** and find the **Inference hooks** section. Turn on **Allow for your organization**. Turning this on unlocks the Inference hooks settings page and always forces **Enforce verdicts** off, so allowing the feature never starts inspection by itself: even a configuration that previously had enforcement on stays uninspected until you turn **Enforce verdicts** back on in the final step. Still in **Data and privacy**, open the **Inference hooks** section to reach the Inference hooks settings page. It lives under Data and privacy rather than as its own entry in the settings nav, so its breadcrumb reads **Data and privacy / Inference hooks**. Until you save an endpoint, the page warns that prompts aren't being inspected yet, and **Enforce verdicts** stays off with a **Requires endpoint** badge. Click **Configure** to open the **Configure endpoint** dialog and fill in: * **Endpoint URL:** the `https://` URL that receives verdict requests. Only `https://` URLs are accepted. * **Custom request headers:** up to 16 static headers sent with every verdict request so your AI security server can authenticate the caller. Header values are stored encrypted and never shown again; after saving, only the header names are displayed. Because values are write-only, saving any change to the headers requires re-entering every value. Changing the endpoint URL clears all stored header values so your credentials are never sent to a new destination; re-enter them after a URL change. Header names must use standard HTTP token characters with `-` rather than `_`, and must not collide with reserved names (request-framing headers such as `Content-*` and `Host`, proxy and cookie headers, client-address headers such as `X-Forwarded-*`, the `webhook-*` signature headers, and the `X-Anthropic-*` prefix). Values must be printable ASCII. The dialog covers only those two fields plus **Test connection**; it doesn't ask about failure handling, which you choose in step 6. Once an endpoint is saved, the button reads **Edit**. Click **Test connection**. Claude sends a synthetic test prompt to the URL and headers currently in the form, not the saved values, so re-enter any stored header values before testing. On success, the result reports whether your AI security server returned an allow or a deny verdict for the test prompt, which surfaces a deny-everything default before you start enforcing. Common failure results: | Result | What to check | | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | URL rejected | The URL failed a structural check. Use an `https://` URL on port 443. | | Private or internal IP | The host resolves to a private or internal address. Use a publicly routable host. | | Timeout | The AI security server did not return a verdict within the timeout. | | Transport error | DNS resolution, the TLS handshake, or the connection failed. | | Non-200 status | The AI security server responded with a status other than 200. Verdicts must come back as HTTP 200; redirects are not followed and count as failures. | | Unparseable response | The AI security server responded, but the body is not a valid verdict. | Save the endpoint configuration. The first save generates your webhook signing secret and reveals it once. Copy it and store it securely before closing the dialog: the secret cannot be retrieved later, only [rotated](https://platform.claude.com/docs/en/manage-claude/inference-hooks-configuration#rotate-your-signing-secret). Your AI security server uses this secret to verify the signature on every request it receives. For the verification procedure, see [Verify the signature](https://platform.claude.com/docs/en/manage-claude/inference-hooks-endpoint#verify-the-signature). Under **Failure handling**, set **Mode** to choose what happens while the AI security server is unreachable or verdicts time out: * **Block the request:** stop inference when your AI security server can't deliver a verdict (fail closed). * **Allow the request:** let the request proceed to the model without inspection (fail open). The dropdown's third option, **Shadow mode**, is a rollout tool rather than a failure policy; see [Shadow mode](https://platform.claude.com/docs/en/manage-claude/inference-hooks-configuration#shadow-mode). Then set **Prompt verdict timeout (ms)**: 1 to 10,000ms, with a default of 5,000ms. The budget covers the entire exchange, and a slower verdict counts as an unreachable server, so set the lowest value your server can reliably meet. Changes in this section save as you make them. On first save, the defaults are **Allow the request** and 5,000ms. Under **Rollout**, set **Requests inspected (%)** to run inspection on a percentage of requests while you bring your AI security server up. The value ranges from 0 to 100: 100 inspects everything, and 0 turns inspection off. Each request rolls once for its whole conversation turn, so a single conversation can be partially inspected across turns. Requests outside the sampled percentage proceed without inspection, even when failure handling is set to **Block the request**. To evaluate verdicts against live traffic without blocking anyone at first, set **Mode** to **Shadow mode** (step 6) before turning on enforcement; see [Shadow mode](https://platform.claude.com/docs/en/manage-claude/inference-hooks-configuration#shadow-mode). Turn on **Enforce verdicts** to gate Claude on your AI security server's verdict for every governed prompt, then confirm in the dialog, which restates your failure handling choice. Allow about a minute for the change to reach every Anthropic server; requests already in flight finish under the old setting. Turning it off stops prompts from being sent to your AI security server, again within about a minute; your configuration is kept. ## Shadow mode Shadow mode runs your hook against live traffic without blocking anything. Your AI security server receives governed prompts and returns verdicts exactly as it would when enforcing, but nothing is blocked: every request proceeds to the model, even when your server denies it or can't be reached, and the end user sees nothing. Use it to tune your policy against your organization's real traffic before you start enforcing. To use shadow mode, set **Mode** to **Shadow mode** under **Failure handling**, then turn on **Enforce verdicts** so prompts flow to your AI security server. While it is active, the settings page shows a **Shadow mode — not blocking** badge. To leave shadow mode, set **Mode** back to **Allow the request** or **Block the request**; verdicts are enforced again once enforcement is on. ## Exclusions Under **Exclusions**, select roles whose members are not covered by Inference hooks: their prompts are never sent to your AI security server. Only custom roles your organization created can be excluded; the built-in roles aren't offered. Pick them in the role selector, whose placeholder reads **Select roles to exclude**, and manage who holds each role from the roles admin page (**Manage roles**); changing exclusions requires identity management permission. The list is empty by default, and with no roles excluded, every governed request is inspected. Exclusion applies to a user's interactive sessions; traffic authenticated by machine credentials is always inspected. If Claude can't resolve a requester's role membership, the request fails closed with a retryable error rather than proceeding uninspected. Changes to the exclusion list are recorded in the audit trail. ## Custom blocked prompt message Under **Custom blocked prompt message**, set custom text of up to 500 characters that is appended to the error an end user sees when your AI security server denies a request (typically who to contact or where to request an exception). The final message is your AI security server's per-request `deny_reason` (when present), a blank line, then this text. With no custom text configured, a built-in default directs the user to contact their administrators; you can also switch the appended message off entirely so the user sees only the `deny_reason`. ## Monitor your AI security server The endpoint health area of the Inference hooks settings page shows: * **Endpoint status:** Healthy, Tripped, Not enforcing, or Not configured before an endpoint is saved. * **Failures per minute:** webhook failures over the last two minutes, averaged. * **Block rate:** denials as a share of your AI security server's verdicts, shown while the rollout percentage is below 100. * **Circuit breaker tripped:** when the breaker last tripped, if it has. * **Recent errors:** each entry is reduced to a timestamp, an error type, and a one-line reason. Entries never include request content or your endpoint URL. The panel is best-effort: if Anthropic cannot read the counters it shows zero failures and no errors rather than an error of its own, so a healthy-looking panel is not by itself proof that your AI security server is healthy. **Failures per minute** counts every failure, including the network and DNS errors that never trip the circuit breaker, so it can be high while **Circuit breaker tripped** stays empty. ## Circuit breaker Sustained webhook failures attributable to your AI security server trip the circuit breaker, which stops enforcement: your server is no longer contacted, and your **Failure handling** choice applies to every inspected request. With **Block the request** selected, users in your organization are blocked until you act. When the breaker trips, administrators are also notified in the claude.ai notification center. To recover, fix the server, then turn **Enforce verdicts** back on to reset the breaker. ## Rotate your signing secret Click **Rotate secret** under **Request signing** to replace your signing secret. Rotation is an immediate cutover: the new secret is generated and revealed once, the old secret can no longer be retrieved, and no request is ever signed with both secrets, so there is no overlap period to rely on. Requests signed with the previous secret can still arrive briefly after rotation; [Verify the signature](https://platform.claude.com/docs/en/manage-claude/inference-hooks-endpoint#verify-the-signature) covers how your AI security server should handle the switchover. ## Audit trail Inference hooks activity is recorded in your organization's [Activity Feed](https://platform.claude.com/docs/en/manage-claude/compliance-activity-feed): configuration changes, denials, and requests that proceeded without inspection under your failure handling setting. Denial records carry identifiers that let you join each denial to the matching record in your own system. ## Turn Inference hooks off There are two levels of off: * **Enforce verdicts** off, on the Inference hooks settings page: within about a minute, prompts from your organization stop being sent to your AI security server; requests already in flight finish under the old setting. The settings page stays available, so use this to pause enforcement while you work on your AI security server. * **Allow for your organization** off, in **Data and privacy** settings: prompts are no longer inspected, and the Inference hooks settings become unavailable until you turn it back on. Your endpoint configuration, custom headers, and signing secret are kept either way; turning it back on forces **Enforce verdicts** off, so turn enforcement on again when you are ready. ## Next steps Build the AI security server: the request and verdict schemas, signature verification, and operational semantics. What Inference hooks are, how the verdict round trip works, and what gets sent to your AI security server. --- title: Develop an Inference hooks integration url: https://platform.claude.com/docs/en/manage-claude/inference-hooks-endpoint description: Build the AI security server that receives signed Inference hooks requests, verifies them, and returns allow or deny verdicts. --- Inference hooks are in beta and available to Claude Enterprise organizations. Field names, request shapes, and headers may change before general availability. An Inference hooks integration is an AI security server: an HTTPS service that Anthropic calls. For each governed request, your server receives a signed `POST` carrying the conversation transcript and responds with an allow or deny verdict. This page documents the protocol for building that server: the request and verdict schemas, signature verification, and the operational contract. For turning Inference hooks on and pointing them at your endpoint, see [Configure Inference hooks](https://platform.claude.com/docs/en/manage-claude/inference-hooks-configuration). For what Inference hooks are and when to use them, see the [Inference hooks overview](https://platform.claude.com/docs/en/manage-claude/inference-hooks). ## Get a first verdict round trip The smallest working integration is a server that reads each request and allows it. Run one of the following servers, expose it at a public `https://` URL (for example, behind a TLS-terminating reverse proxy on a host you control, not a reverse-tunnel service; see [Receive a request](https://platform.claude.com/docs/en/manage-claude/inference-hooks-endpoint#receive-a-request)), then have your administrator [set it as the endpoint and test the connection](https://platform.claude.com/docs/en/manage-claude/inference-hooks-configuration): the **Test connection** result reports the allow verdict your server returned. ```python Python # Run with: python server.py from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer class VerdictHandler(BaseHTTPRequestHandler): protocol_version = "HTTP/1.1" # keep the connection open between verdicts def do_POST(self): # Drain the body; transcripts can be megabytes. self.rfile.read(int(self.headers.get("Content-Length", 0))) verdict = b'{"action": "allow"}' self.send_response(200) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(verdict))) self.end_headers() self.wfile.write(verdict) ThreadingHTTPServer(("", 8000), VerdictHandler).serve_forever() ``` ```typescript TypeScript // Run with: node server.ts import { createServer } from "node:http"; createServer((request, response) => { // Drain the body before answering; transcripts can be megabytes. request.resume(); request.on("end", () => { response.writeHead(200, { "Content-Type": "application/json" }); response.end('{"action": "allow"}'); }); }).listen(8000); ``` ```csharp C# #:sdk Microsoft.NET.Sdk.Web #:property PublishAot=false // Run with: dotnet run server.cs var app = WebApplication.Create(); app.MapPost("/{**path}", async (HttpRequest request) => { // Drain the body; transcripts can be megabytes. await request.Body.CopyToAsync(Stream.Null); return Results.Text("""{"action": "allow"}""", "application/json"); }); app.Run("http://0.0.0.0:8000"); ``` ```go Go // Run with: go run server.go package main import ( "io" "log" "net/http" ) func main() { http.HandleFunc("POST /", func(writer http.ResponseWriter, request *http.Request) { // Drain the body so the connection can be reused; transcripts can be megabytes. io.Copy(io.Discard, request.Body) writer.Header().Set("Content-Type", "application/json") writer.Write([]byte(`{"action": "allow"}`)) }) log.Fatal(http.ListenAndServe(":8000", nil)) } ``` ```java Java // Run with: java VerdictServer.java import com.sun.net.httpserver.HttpServer; void main() throws IOException { HttpServer server = HttpServer.create(new InetSocketAddress(8000), 0); server.createContext("/", exchange -> { // Drain the body without buffering it; transcripts can be megabytes. exchange.getRequestBody().transferTo(OutputStream.nullOutputStream()); byte[] verdict = "{\"action\": \"allow\"}".getBytes(StandardCharsets.UTF_8); exchange.getResponseHeaders().set("Content-Type", "application/json"); exchange.sendResponseHeaders(200, verdict.length); try (OutputStream responseBody = exchange.getResponseBody()) { responseBody.write(verdict); } }); server.setExecutor(Executors.newVirtualThreadPerTaskExecutor()); server.start(); } ``` ```php PHP These servers accept every request, including unsigned ones. Add [signature verification](https://platform.claude.com/docs/en/manage-claude/inference-hooks-endpoint#verify-the-signature) before you enforce. ## Receive a request Anthropic sends an HTTPS `POST` to the URL your administrator configures. The whole configured URL is the endpoint: there is no fixed path suffix, so choose any path that suits your server. Host your AI security server where Anthropic can reach it: an `https://` URL on port 443, on a publicly routable host (private, loopback, and carrier-grade NAT ranges are refused at connect time), with a certificate that validates against the public CA trust store, responding without redirects. The configured URL must be the final destination. Reverse-tunnel hosts (ngrok and similar tunnel services) are not supported: Anthropic's network policy blocks them. Host your server on a domain you control. [Configure Inference hooks](https://platform.claude.com/docs/en/manage-claude/inference-hooks-configuration) covers how your administrator sets and tests the URL. Every request carries these fixed headers, along with any [custom request headers](https://platform.claude.com/docs/en/manage-claude/inference-hooks-configuration) your administrator configured and, once your organization has a signing secret, the `webhook-*` signature headers described in [Verify the signature](https://platform.claude.com/docs/en/manage-claude/inference-hooks-endpoint#verify-the-signature): | Header | Value | | ----------------- | ------------------ | | `Content-Type` | `application/json` | | `User-Agent` | `anthropic-dlp/1` | | `Accept-Encoding` | `identity` | There is one hook event today: the prompt frame, sent once per governed inference request, before inference begins. Anthropic holds the request until your AI security server responds or the verdict timeout elapses. ## The prompt frame The request body is a JSON object with these fields: | Field | Type | Description | | ------------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `type` | string | The hook event. Always `"prompt"` today; other event types will be introduced in the future, so handle an unrecognized value gracefully (see [Forward compatibility](https://platform.claude.com/docs/en/manage-claude/inference-hooks-endpoint#forward-compatibility)). | | `request_id` | string | Opaque per-inference-call identifier for correlation. Equals the `webhook-id` header. | | `tenant_id` | string or null | Opaque identifier for the organization the request belongs to. | | `actor` | object | The principal the request is attributed to, discriminated on `type` (`"user"` is the only value sent today): `id` (a tagged identifier, stable across requests for the same account) and `email_address` (when available). Both `id` and `email_address` can be null. | | `source` | object | The originating application: `application` (see [Source values](https://platform.claude.com/docs/en/manage-claude/inference-hooks-endpoint#source-values)). | | `messages` | array | The conversation transcript up to the point of inference. See [Content blocks](https://platform.claude.com/docs/en/manage-claude/inference-hooks-endpoint#content-blocks). | | `session_id` | string or null | Opaque conversation identifier, when one exists. Don't parse it. For Claude Code it is a best-effort, client-asserted session identifier. | | `model` | string or null | Public model identifier for this request, when available. | | `metadata` | object | Reserved extension map of string keys to string values, sent empty today. Require nothing from it, and tolerate its absence, its presence, and any keys that appear. | Requests currently also carry deprecated legacy aliases of some of these fields. Read the field names documented on this page and ignore any others; the aliases exist only for earlier integrations. An example request body: ```json { "type": "prompt", "request_id": "req_abc123", "tenant_id": "11111111-1111-1111-1111-111111111111", "actor": { "type": "user", "id": "user_01AbCdEfGhIjKlMnOpQrStUv", "email_address": "alice@example.com" }, "source": { "application": "claude-ai" }, "session_id": "22222222-2222-2222-2222-222222222222", "model": "claude-sonnet-4-5", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Summarize the attached report." }, { "type": "attachment", "file_name": "q2-report.pdf", "media_type": "application/pdf", "size_bytes": 48213, "text": "Q2 revenue grew 14% quarter over quarter..." } ] } ], "metadata": {} } ``` ### Content blocks Each entry in `messages` has a `role` of `user` or `assistant` (tool results appear under the `user` role, matching the public Messages API content model) and a `content` array of blocks discriminated by `type`: | Block `type` | Fields | | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `text` | `text`: the text content. | | `tool_use` | `id`: the identifier the matching tool result references. `tool_name`: the tool's name. `input`: the arguments the model passed to the tool. | | `tool_result` | `content`: the tool's output as text, with parts joined by newlines; binary parts such as images are replaced by placeholder markers, and raw bytes are never sent. `is_error`: whether the tool call failed. `tool_name`: the tool's name, so a policy can condition on tool identity without cross-referencing an earlier block. `tool_use_id`: the `id` of the matching `tool_use` block. | | `attachment` | `file_name`: the original file name or path. `media_type`: the attachment's media type. `size_bytes`: the size of the original file. `text`: the text content of the attachment when available, such as extracted document text, an audio transcript, or link metadata. Raw attachment bytes are never sent. | A block whose `type` you don't recognize is a forward-compatible addition. The only field it guarantees is `type`; your policy may inspect whatever other fields are present, but must not reject the request because of an unrecognized type. ### What the transcript contains The transcript is the conversation as the end user sees it, up to the point of inference: transcript text, tool calls and their results, extracted attachment text, and prior turns. It never includes system prompts, tool definitions, Anthropic-internal context, Claude's hidden reasoning, or raw file bytes. A turn whose every block is excluded is omitted entirely, so don't assume strict user and assistant alternation. Transcripts are sent untruncated, so a long conversation with large attachments produces a large request body, up to an upper bound of 10 MB. Raise your server's body limit to accept that ceiling. Several common defaults are much smaller, including nginx `client_max_body_size` at 1 MB and Express `express.json()` at 100 kB, and a rejected body counts as a webhook failure, so under **Allow the request** failure handling an oversized prompt would reach the model uninspected. ### Source values `source.application` is an open string, not a closed enum. Known values are `claude-ai` and `claude-code`; [connection tests](https://platform.claude.com/docs/en/manage-claude/inference-hooks-configuration) use `config-test`. New values may appear, and your server must not reject a request because of one it doesn't recognize. Treat `source.application` as advisory routing metadata, not a trust boundary: don't rest a security-critical policy decision on it alone. ## Return a verdict Respond with HTTP 200 and a JSON verdict body for both outcomes; the `action` field discriminates. To allow the request: ```json { "action": "allow" } ``` To deny it: ```json { "action": "deny", "deny_reason": "This prompt appears to contain customer payment card data, which your organization's policy does not allow.", "reference_id": "scan_01HXPT4R9V" } ``` | Field | Constraints | Semantics | | -------------- | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `action` | `"allow"` or `"deny"`; required | `allow` lets inference proceed; `deny` rejects it. | | `deny_reason` | string or null; at most 500 characters, longer values truncated | Shown to the end user when `action` is `deny`; ignored on `allow`. | | `reference_id` | string or null; at most 50 characters from `[A-Za-z0-9._:/-]` | Your own identifier for this evaluation. It's recorded on the denial's `inference_hooks_request_denied` [compliance activity](https://platform.claude.com/docs/en/manage-claude/compliance-activity-feed) and never shown to the end user. Keep it opaque: no request content and no personal data. | A deny is never discarded over a formatting problem: an oversize `deny_reason` is truncated, a malformed `reference_id` is silently dropped, and the `action` is still honored. The reverse doesn't hold. Anything other than HTTP 200 with a parseable verdict is a webhook failure, and your organization's [failure handling](https://platform.claude.com/docs/en/manage-claude/inference-hooks-configuration) applies instead of a verdict. In particular: * Don't signal a deny with an error status. A non-200 response is a failure, not a deny. * Any `action` value other than `allow` or `deny` is treated as a webhook failure. Anthropic reads at most 64 KiB of the response body, and the body must be uncompressed. Redirects are not followed, and cookies are ignored. Unknown fields in the verdict body are ignored, so you can return a richer object alongside the fields documented here. ## Verify the signature Requests are signed per the [Standard Webhooks](https://www.standardwebhooks.com/) specification, using three headers. Anthropic sends the header names in lowercase, and proxies are free to re-case them, so look them up case-insensitively. | Header | Contents | | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `webhook-id` | Unique identifier for this delivery. Equals the body's `request_id`. Use it as an idempotency key and as the first component of the signed payload. | | `webhook-timestamp` | Unix time in seconds, as a decimal string, when the request was signed. Reject a timestamp more than five minutes from your server's clock, in either direction. | | `webhook-signature` | One or more space-separated `v1,` values, each an HMAC-SHA256 over `{webhook-id}.{webhook-timestamp}.{raw body bytes}`. Accept the request if any value matches yours, using a constant-time comparison. | Two details cause most verification bugs: * **Verify raw bytes.** Compute the HMAC over the body exactly as received, before any JSON parsing or re-encoding. * **Decode the secret with a standard base64 decoder.** The signing secret is the value after the `whsec_` prefix, encoded with the standard base64 alphabet (`+` and `/`), as is the signature in the header. A URL-safe decoder derives the wrong key bytes whenever the secret contains `+` or `/`, which is most of the time. Once your organization has a signing secret, every request Anthropic sends is signed, and [enabling Inference hooks requires one](https://platform.claude.com/docs/en/manage-claude/inference-hooks-configuration), so reject any request that arrives unsigned. One exception: a connection test sent before your organization's first save arrives unsigned, because the signing secret doesn't exist yet. Accept unsigned requests until your administrator confirms the secret exists, then reject them. [Rotating the secret](https://platform.claude.com/docs/en/manage-claude/inference-hooks-configuration#rotate-your-signing-secret) is an immediate cutover, but requests signed with the previous secret can still arrive for about a minute afterward, plus anything already in flight. Have your AI security server accept signatures from both secrets during the switchover so those stragglers aren't rejected. The following samples are server implementations, so there is no shell tab: an AI security server is a long-running HTTPS service rather than a one-shot request. Each sample uses only the language's standard library; the [Standard Webhooks](https://www.standardwebhooks.com/) project also publishes verification libraries for most languages. ```python Python import base64 import hashlib import hmac import time TOLERANCE_SECONDS = 300 def verify(secret: str, headers: dict[str, str], body: bytes) -> bool: """Return True if the body was signed by Anthropic for this organization. Anthropic sends header names in lowercase, but proxies are free to re-case them, so normalize the lookup to lowercase. """ lowercased = {name.lower(): value for name, value in headers.items()} try: message_id = lowercased["webhook-id"] timestamp = lowercased["webhook-timestamp"] signatures = lowercased["webhook-signature"] except KeyError: return False # unsigned request: not from Anthropic try: signed_at = int(timestamp) except ValueError: return False if abs(time.time() - signed_at) > TOLERANCE_SECONDS: return False # replayed, or the clocks disagree try: key = base64.b64decode(secret.removeprefix("whsec_"), validate=True) except ValueError: return False # misconfigured secret: reject rather than crash payload = f"{message_id}.{timestamp}.".encode() + body expected = b"v1," + base64.b64encode( hmac.new(key, payload, hashlib.sha256).digest() ) # Compare bytes: compare_digest on str raises on non-ASCII input. return any( hmac.compare_digest(expected, candidate.encode()) for candidate in signatures.split() ) ``` ```typescript TypeScript import { createHmac, timingSafeEqual } from "node:crypto"; import type { IncomingHttpHeaders } from "node:http"; const TOLERANCE_SECONDS = 300; /** * Returns true if the body was signed by Anthropic for this organization. * * Node lowercases incoming header names, matching how Anthropic sends * them, so look them up in lowercase. */ export function verify(secret: string, headers: IncomingHttpHeaders, body: Buffer): boolean { const messageId = headers["webhook-id"]; const timestamp = headers["webhook-timestamp"]; const signatures = headers["webhook-signature"]; if ( typeof messageId !== "string" || typeof timestamp !== "string" || typeof signatures !== "string" ) { return false; // unsigned request: not from Anthropic } const signedAt = Number(timestamp); if ( !Number.isFinite(signedAt) || Math.abs(Date.now() / 1000 - signedAt) > TOLERANCE_SECONDS ) { return false; // replayed, or the clocks disagree } const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64"); const payload = Buffer.concat([Buffer.from(`${messageId}.${timestamp}.`), body]); const expected = Buffer.from( "v1," + createHmac("sha256", key).update(payload).digest("base64") ); return signatures.split(" ").some((candidate) => { const candidateBytes = Buffer.from(candidate); return ( candidateBytes.length === expected.length && timingSafeEqual(candidateBytes, expected) ); }); } ``` ```csharp C# using System.Security.Cryptography; using System.Text; static class InferenceHooks { private const int ToleranceSeconds = 300; /// /// Returns true if the body was signed by Anthropic for this organization. /// Anthropic sends header names in lowercase, but proxies are free to /// re-case them, so match them case-insensitively. /// public static bool Verify(string secret, IReadOnlyDictionary headers, byte[] body) { // TryAdd keeps the first value if a proxy delivered case-duplicate // names; the copying constructor would throw on them instead. var lookup = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var (name, value) in headers) { lookup.TryAdd(name, value); } if (!lookup.TryGetValue("webhook-id", out var messageId) || !lookup.TryGetValue("webhook-timestamp", out var timestamp) || !lookup.TryGetValue("webhook-signature", out var signatures)) { return false; // unsigned request: not from Anthropic } if (!long.TryParse(timestamp, out var signedAt) || Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - signedAt) > ToleranceSeconds) { return false; // replayed, or the clocks disagree } // Standard base64 alphabet: a URL-safe decoder derives the wrong key bytes. var encodedKey = secret.StartsWith("whsec_") ? secret["whsec_".Length..] : secret; byte[] key; try { key = Convert.FromBase64String(encodedKey); } catch (FormatException) { return false; // misconfigured secret: reject rather than crash } byte[] payload = [.. Encoding.UTF8.GetBytes($"{messageId}.{timestamp}."), .. body]; var expected = Encoding.UTF8.GetBytes( "v1," + Convert.ToBase64String(HMACSHA256.HashData(key, payload))); // FixedTimeEquals is constant-time and returns false on a length mismatch. return signatures.Split(' ', StringSplitOptions.RemoveEmptyEntries).Any(candidate => CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(candidate), expected)); } } ``` ```go Go package hooks import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "net/http" "strconv" "strings" "time" ) const toleranceSeconds = 300 // verify reports whether body was signed by Anthropic for this organization. // net/http canonicalizes header names on lookup, so re-cased names still match. func verify(secret string, header http.Header, body []byte) bool { messageID := header.Get("webhook-id") timestamp := header.Get("webhook-timestamp") signatures := header.Get("webhook-signature") if messageID == "" || timestamp == "" || signatures == "" { return false // unsigned request: not from Anthropic } signedAt, err := strconv.ParseInt(timestamp, 10, 64) if err != nil { return false } age := time.Now().Unix() - signedAt if age > toleranceSeconds || age < -toleranceSeconds { return false // replayed, or the clocks disagree } // Standard base64 alphabet: a URL-safe decoder derives the wrong key bytes. key, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(secret, "whsec_")) if err != nil { return false } mac := hmac.New(sha256.New, key) mac.Write([]byte(messageID + "." + timestamp + ".")) mac.Write(body) expected := "v1," + base64.StdEncoding.EncodeToString(mac.Sum(nil)) for _, candidate := range strings.Fields(signatures) { if hmac.Equal([]byte(candidate), []byte(expected)) { // constant-time return true } } return false } ``` ```java Java import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.security.MessageDigest; import java.time.Instant; import java.util.Base64; import java.util.HashMap; import java.util.Locale; import java.util.Map; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; public final class InferenceHookVerifier { private static final long TOLERANCE_SECONDS = 300; /** * Returns true if the body was signed by Anthropic for this organization. * *

Anthropic sends header names in lowercase, but proxies are free to * re-case them, so normalize the lookup to lowercase. */ public static boolean verify(String secret, Map headers, byte[] body) { Map lowercased = new HashMap<>(); headers.forEach((name, value) -> lowercased.put(name.toLowerCase(Locale.ROOT), value)); String messageId = lowercased.get("webhook-id"); String timestamp = lowercased.get("webhook-timestamp"); String signatures = lowercased.get("webhook-signature"); if (messageId == null || timestamp == null || signatures == null) { return false; // unsigned request: not from Anthropic } long signedAt; try { signedAt = Long.parseLong(timestamp); } catch (NumberFormatException _) { return false; } if (Math.abs(Instant.now().getEpochSecond() - signedAt) > TOLERANCE_SECONDS) { return false; // replayed, or the clocks disagree } // Standard base64 alphabet: a URL-safe decoder derives the wrong key bytes. byte[] key; try { key = Base64.getDecoder().decode( secret.startsWith("whsec_") ? secret.substring("whsec_".length()) : secret); } catch (IllegalArgumentException _) { return false; // misconfigured secret: reject rather than crash } byte[] expected; try { Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(key, "HmacSHA256")); mac.update((messageId + "." + timestamp + ".").getBytes(StandardCharsets.UTF_8)); expected = ("v1," + Base64.getEncoder().encodeToString(mac.doFinal(body))) .getBytes(StandardCharsets.UTF_8); } catch (GeneralSecurityException impossible) { // Every JVM ships HmacSHA256, so this never fires at runtime. throw new IllegalStateException(impossible); } for (String candidate : signatures.split(" ")) { if (MessageDigest.isEqual(candidate.getBytes(StandardCharsets.UTF_8), expected)) { return true; // MessageDigest.isEqual is constant-time } } return false; } } ``` ```php PHP const TOLERANCE_SECONDS = 300; /** * Returns true if the body was signed by Anthropic for this organization. * * Anthropic sends header names in lowercase, but proxies are free to * re-case them, so normalize the lookup to lowercase. */ function verify(string $secret, array $headers, string $body): bool { $lowercased = array_change_key_case($headers, CASE_LOWER); $messageId = $lowercased['webhook-id'] ?? null; $timestamp = $lowercased['webhook-timestamp'] ?? null; $signatures = $lowercased['webhook-signature'] ?? null; if ($messageId === null || $timestamp === null || $signatures === null) { return false; // unsigned request: not from Anthropic } $signedAt = filter_var($timestamp, FILTER_VALIDATE_INT); if ($signedAt === false || abs(time() - $signedAt) > TOLERANCE_SECONDS) { return false; // replayed, or the clocks disagree } // Standard base64 alphabet: a URL-safe decoder derives the wrong key bytes. $encodedKey = str_starts_with($secret, 'whsec_') ? substr($secret, strlen('whsec_')) : $secret; $key = base64_decode($encodedKey, strict: true); if ($key === false) { return false; } $payload = "{$messageId}.{$timestamp}." . $body; $expected = 'v1,' . base64_encode(hash_hmac('sha256', $payload, $key, binary: true)); foreach (explode(' ', $signatures) as $candidate) { if (hash_equals($expected, $candidate)) { // constant-time return true; } } return false; } ``` ```ruby Ruby # base64 is a bundled gem in Ruby 3.4: Bundler-managed apps add gem "base64". require "base64" require "openssl" TOLERANCE_SECONDS = 300 # Returns true if the body was signed by Anthropic for this organization. # # Anthropic sends header names in lowercase, but proxies are free to # re-case them, so normalize the lookup to lowercase. def verify(secret, headers, body) lowercased = headers.transform_keys(&:downcase) message_id = lowercased["webhook-id"] timestamp = lowercased["webhook-timestamp"] signatures = lowercased["webhook-signature"] if message_id.nil? || timestamp.nil? || signatures.nil? return false # unsigned request: not from Anthropic end signed_at = Integer(timestamp, exception: false) if signed_at.nil? || (Time.now.to_i - signed_at).abs > TOLERANCE_SECONDS return false # replayed, or the clocks disagree end # Standard base64 alphabet: a URL-safe decoder derives the wrong key bytes. begin key = Base64.strict_decode64(secret.delete_prefix("whsec_")) rescue ArgumentError return false # misconfigured secret: reject rather than crash end # Feed the body separately so its encoding never has to match the prefix's. hmac = OpenSSL::HMAC.new(key, "SHA256") hmac.update("#{message_id}.#{timestamp}.") hmac.update(body) expected = "v1," + Base64.strict_encode64(hmac.digest) signatures.split(" ").any? do |candidate| # fixed_length_secure_compare raises on a length mismatch, so screen lengths first. candidate.bytesize == expected.bytesize && OpenSSL.fixed_length_secure_compare(candidate, expected) end end ``` ## Operational semantics ### Timeout and retry Your administrator sets a verdict timeout between 1 and 10,000ms (5,000ms by default). The budget covers the entire exchange: connection, TLS handshake, request, and response. Anthropic retries exactly once, after a 100ms delay, and only when the connection attempt fails. The retry shares the same timeout budget and carries the same `webhook-id` and the same signature. Once your AI security server has responded, the exchange is never retried. ### Webhook failures Timeouts, non-200 statuses (redirects included), unparseable or oversized response bodies, and unreachable endpoints are all webhook failures. A webhook failure never becomes a deny; instead, your organization's [failure handling](https://platform.claude.com/docs/en/manage-claude/inference-hooks-configuration) setting decides whether the affected request is blocked or proceeds without inspection. ### Circuit breaker Sustained webhook failures attributable to your AI security server trip a circuit breaker that stops enforcement: Anthropic stops contacting your server, and failure handling applies to every request. Recovery happens on the admin side: fix the server, then have your administrator turn **Enforce verdicts** back on. See [Circuit breaker](https://platform.claude.com/docs/en/manage-claude/inference-hooks-configuration#circuit-breaker). ### Latency Enforcement adds your AI security server's round trip to the latency of every governed request in your organization. Keep the verdict fast, and load-test your server before rolling it out to a large organization. ### Source IP addresses Requests to your AI security server originate from `160.79.106.0/24`, part of Anthropic's published [outbound IP ranges](https://platform.claude.com/docs/en/api/ip-addresses). Allowlist that block, not the inbound ranges on the same page, which don't cover it. Allowlisting narrows your server's exposure, but it is not a substitute for signature verification: the block carries Anthropic egress traffic beyond Inference hooks. ## Forward compatibility The protocol grows without breaking correctly written servers. Your server must ignore: * Unknown top-level fields on the prompt frame. * Unknown keys in `metadata`. * New `source.application` values. * New `actor.type` values. `actor` is a union discriminated on `type`, and `"user"` is the only kind sent today; a future kind guarantees only that `type` is present. * Content blocks with an unrecognized `type`. Never reject a request because of an unrecognized block type or field; read the fields you know and skip the rest. Other hook event types will be introduced in the future. A new event type is an addition your server can't handle by skipping a field: the request still needs a verdict. When the top-level `type` is a value you don't recognize, return an allow verdict rather than an error status; an error response is a [webhook failure](https://platform.claude.com/docs/en/manage-claude/inference-hooks-endpoint#webhook-failures), and sustained failures trip the [circuit breaker](https://platform.claude.com/docs/en/manage-claude/inference-hooks-endpoint#circuit-breaker). ## Design your integration A production AI security server makes a few design choices beyond the wire protocol. **Deduplicate on `webhook-id`.** The `webhook-id` header is unique per delivery and equals the body's `request_id`, and a connection-failure retry reuses it, so it works as an idempotency key. If you record verdicts, key the records on it. **Record verdicts and join denials.** Store each verdict you return along with its `reference_id`. Every denial is recorded as an `inference_hooks_request_denied` compliance activity carrying the `reference_id` your server returned, so you can join denials in the [Activity Feed](https://platform.claude.com/docs/en/manage-claude/compliance-activity-feed) to the matching records in your own system. **Archive with an always-allow server.** To capture transcripts in real time without policing them, return `{"action": "allow"}` unconditionally and persist the frame after responding. This is a push-based alternative to polling the [Compliance API](https://platform.claude.com/docs/en/manage-claude/compliance-api), and answering before you persist keeps your round trip out of the user's critical path. **Write `deny_reason` for the end user.** The text you return is what the user sees when their request is blocked, truncated at 500 characters. Tell them what to change, such as which kind of content to remove, rather than emitting a scanner code that only your team can interpret. ## Next steps Enable Inference hooks, connect and test your endpoint, and control enforcement, failure handling, and rollout. What Inference hooks are, how the verdict round trip works, and when to use them. --- title: Inference hooks url: https://platform.claude.com/docs/en/manage-claude/inference-hooks description: Send each governed prompt to your organization's AI security server for an allow or deny verdict before inference proceeds. --- Inference hooks are in beta and available to Claude Enterprise organizations. Configuring them requires the `organization:manage` permission in claude.ai, which the built-in Admin, Owner, and Primary owner roles hold; see [Configure Inference hooks](https://platform.claude.com/docs/en/manage-claude/inference-hooks-configuration). Inference hooks let a Claude Enterprise organization route every governed prompt through an AI security server, an HTTPS service that the organization or its security vendor operates, before inference runs. When a user submits a prompt, Anthropic sends the conversation transcript to your AI security server and waits for an allow or deny verdict; a denied request never reaches the model. Security and compliance teams use Inference hooks to enforce data policies inline, and developers build the AI security server that evaluates each request. Because the hook runs on Anthropic's servers, after the request leaves the client and before the model runs, it applies to every governed request uniformly, with nothing to install or deploy on user devices. Today the only hook event is `prompt`, which fires once per governed inference request, before inference begins. Response-side enforcement is planned as a later event. *** ## How Inference hooks work 1. A user submits a prompt on a governed surface. 2. Anthropic sends an HTTPS `POST` to your organization's configured AI security server endpoint. The request body carries the conversation transcript, and each request is signed according to the [Standard Webhooks](https://www.standardwebhooks.com/) specification once your organization generates its signing secret, so your server can verify it came from Anthropic. 3. Your AI security server evaluates the content and responds with a verdict within the verdict timeout your organization configures (5 seconds by default). 4. On `allow`, inference proceeds normally. On `deny`, the request is rejected and the user sees a blocked-by-policy message assembled from two parts: the per-request reason your AI security server supplied in the verdict's `deny_reason` field, followed by a standing message your administrators configure (for example, who to contact or where to request an exception). If your administrators haven't configured one, a built-in default directs the user to contact them. Each denial is also recorded in your organization's [Activity Feed](https://platform.claude.com/docs/en/manage-claude/compliance-activity-feed). The following diagram traces one example (a Cowork request where Claude also calls an O365 tool) to illustrate which parts of the flow are hooked. The hooked points are the diagram's steps 1 and 5, where the prompt arrives and the tool result returns; each results in the validation exchange with your AI security server shown in steps 2 and 6. ![Flow diagram: the AI security server validates both the prompt and the tool result before inference proceeds](https://platform.claude.com/docs/images/inference-hooks-flow.svg) A verdict is a small JSON object: `{"action": "allow"}` lets the request proceed, and a deny carries the user-facing reason. For the full verdict schema, see [Return a verdict](https://platform.claude.com/docs/en/manage-claude/inference-hooks-endpoint#return-a-verdict). Your AI security server sees what the user sees: transcript text, tool calls and their results, and text extracted from attachments. It never receives raw file or image bytes, system prompts, or Anthropic-internal context. If your AI security server is unreachable, returns an error, or doesn't respond within the timeout, your organization's failure handling setting decides the outcome: block the request, or allow it to proceed without inspection. Enforcement can roll out at your pace, so nobody has to be blocked on day one: shadow mode observes verdicts on live traffic without blocking anything, a rollout percentage inspects a chosen fraction of requests, and exclusions exempt members of chosen roles entirely. See [Configure Inference hooks](https://platform.claude.com/docs/en/manage-claude/inference-hooks-configuration). For the full request and response schemas, signature verification, and operational details, see [Develop an integration](https://platform.claude.com/docs/en/manage-claude/inference-hooks-endpoint). *** ## Use cases * **Data loss prevention (DLP).** Forward the transcript to your DLP scanner and deny prompts that carry regulated or classified material. This is the most common deployment. * **Real-time transcript archival.** Archive each transcript as it arrives and always return `allow`, as a push-based alternative to polling the [Compliance API](https://platform.claude.com/docs/en/manage-claude/compliance-api). * **Prompt telemetry.** Measure how your organization uses Claude, at the moment of use. * **Policy engines.** Enforce your own rules before inference: model allowlists, project-scoped restrictions, or working-hours controls. *** ## Current limitations * Attachments are represented by metadata and extracted text. Raw file and image bytes are never sent, so image-only content (for example, a screenshot of a document) is not inspected. * Verdicts are allow or deny. Rewriting or redacting a prompt is not supported. * Platform organizations (API access through the Claude Platform) are out of scope. *** ## Availability Inference hooks are available to Claude Enterprise organizations. Configuring them requires the `organization:manage` permission, which the built-in Admin, Owner, and Primary owner roles hold, as does any custom role granted it. One hook governs conversations across claude.ai, Cowork, and Claude Code sessions in your Claude Enterprise organization, whether they run on the web, in the desktop app, or in the CLI. Inference hooks are not available on Amazon Bedrock or Google Cloud. Governed requests are the inference requests behind the user's conversation. Ancillary requests, such as conversation title generation, aren't sent to your endpoint, and system prompts and tool definitions are never included in what is sent. Voice mode is not covered. *** ## Inference hooks versus the Compliance API Both features serve security, legal, and compliance teams at Claude Enterprise organizations. | | Inference hooks | Compliance API | | ------------ | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | When it acts | Inline, before inference runs | After the fact | | What it does | Allows or denies each governed request in real time | Retrieves activity, chats, files, projects, Cowork and Claude Code session transcripts, and users for audit and export | | Direction | Anthropic calls your AI security server | You call Anthropic's API | Use Inference hooks to stop a request before it reaches the model, and the [Compliance API](https://platform.claude.com/docs/en/manage-claude/compliance-api) to audit what happened afterward. *** ## In this section Allow Inference hooks for your organization, set up and test your AI security server, choose failure handling, and enforce verdicts. The request and verdict schemas, signature verification, operational semantics, and integration patterns for building the AI security server. ### Compliance API --- title: Compliance API url: https://platform.claude.com/docs/en/manage-claude/compliance-api description: Programmatic access to your organization's Claude activity, chats, files, projects, Claude Cowork and Claude Code sessions, and users for compliance, audit, and governance. --- The Compliance API gives Claude Enterprise and Claude Console customers programmatic access to their organization's Activity Feed. For Claude Enterprise organizations, it also covers the directory of users, roles, and groups across every linked organization; the effective settings in force for each organization; the underlying chats, files, and projects in claude.ai organizations; and Cowork and Claude Code sessions. Security, legal, and compliance teams use it to audit activity, retrieve or delete content, and feed events into downstream tooling. Two key types unlock the Compliance API. A **Compliance Access Key** (created in claude.ai) reaches every endpoint, and an **Admin API key** (created in Claude Console) reaches the Activity Feed only. See [Which key do you need?](https://platform.claude.com/docs/en/manage-claude/compliance-api-access#which-key-do-you-need) for the full key-type comparison. The following call returns the most recent activity event in your organization. Any key with the `read:compliance_activities` scope can make it. To create a key and grant it that scope, see [Set up the Compliance API](https://platform.claude.com/docs/en/manage-claude/compliance-api-access). ```bash cURL curl --fail-with-body -sS \ "https://api.anthropic.com/v1/compliance/activities?limit=1" \ --header "x-api-key: $ANTHROPIC_COMPLIANCE_ACCESS_KEY" ``` A successful response returns a JSON object containing `data` (an array of `Activity` records), `has_more`, `first_id`, and `last_id`: ```json Response { "data": [ { "id": "activity_01XyDMpzjS89pFZXqSFUBDr6", "created_at": "2026-04-10T08:09:10Z", "organization_id": "org_01Wv6QeBcDfGhJkLmNpQrSt8", "organization_uuid": "abcdef01-2345-6789-abcd-ef0123456789", "actor": { "type": "user_actor", "email_address": "user@example.com", "user_id": "user_01TuVwXyZaBcDeFgH2JkLmN4", "ip_address": "192.0.2.34", "user_agent": "Mozilla/5.0..." }, "type": "claude_chat_created", "claude_chat_id": "claude_chat_01XyDMpzjS89pFZXqSFUBDr6", "claude_project_id": "claude_proj_01KGp4eZNug9ri4kE35RSppq" } ], "has_more": true, "first_id": "activity_01XyDMpzjS89pFZXqSFUBDr6", "last_id": "activity_01XyDMpzjS89pFZXqSFUBDr6" } ``` *** ## How the Compliance API works Every endpoint lives under `/v1/compliance/*` on `https://api.anthropic.com` and authenticates through the `x-api-key` header. To provision a key, see [Set up the Compliance API](https://platform.claude.com/docs/en/manage-claude/compliance-api-access). The Activity Feed (`GET /v1/compliance/activities`) is available to any key that carries the `read:compliance_activities` scope; see [Query the Activity Feed](https://platform.claude.com/docs/en/manage-claude/compliance-activity-feed) for filters, pagination, and the full `Activity` object. The remaining endpoints require a Compliance Access Key carrying the relevant scope. A Claude Enterprise tenant has one parent organization (the top-level container that centralizes identity) with linked organizations of two kinds: claude.ai organizations, where users chat and store content, and Claude Console organizations, where users manage Claude API workloads. For a key that covers the parent organization, the directory endpoints (organizations, users, roles, and groups) return data from every linked organization of either kind. The content endpoints (chats, files, projects, project attachments, and sessions) serve Claude Enterprise data only. The chat, file, and project endpoints return claude.ai chats, files, and projects. The session endpoints return transcripts of Cowork and Claude Code sessions on users' machines (local sessions), captured while users are signed in with their Claude Enterprise account. They also return transcripts of Cowork sessions started on claude.ai web or mobile, which run in the cloud in Anthropic-managed environments (remote sessions). A standalone Claude Console organization (one with no parent organization) is not part of a Claude Enterprise tenant; it uses Admin API keys and can query the Activity Feed only. All `/v1/compliance/*` endpoints share a rate limit of 600 requests per minute per parent organization (for a standalone Claude Console organization, per organization). The local session endpoints count only against that shared limit, and the remote session endpoints carry a second request budget on top. See [429 Too Many Requests](https://platform.claude.com/docs/en/manage-claude/compliance-errors#429-too-many-requests) for the response headers and retry contract. *** ## Compliance API versus related features A few adjacent features overlap with the Compliance API; here is how to choose. ### Export audit logs The audit log export is a separate feature in [claude.ai > Organization settings > Data and privacy](https://claude.ai/admin-settings/data-privacy-controls) that lets owners and primary owners download a CSV of organization events. It's significantly narrower than the Compliance API: a capped lookback window, CSV download only, and no access to chat, file, or project content. Standardize on the Compliance API for ongoing programmatic use. ### Analytics API Anthropic provides two analytics APIs: the Claude Enterprise Analytics API and the [Claude Code Analytics API](https://platform.claude.com/docs/en/manage-claude/claude-code-analytics-api). Both return aggregated usage and cost figures for IT, FinOps, and platform teams, whereas the Compliance API returns per-event records for security, legal, and compliance teams. The two API families answer different questions, use different keys, and are provisioned separately. ### OpenTelemetry logging [Cowork's OpenTelemetry logging](https://support.claude.com/en/articles/14477985-monitor-claude-cowork-activity-with-opentelemetry) and [Claude Code monitoring](https://code.claude.com/docs/en/monitoring-usage) stream per-event telemetry, including token, cost, and host metadata, to a collector you run as activity happens, whereas the Compliance API returns retained per-session transcripts from Anthropic on request and works with your existing Compliance Access Key. For a side-by-side comparison, see the [Compliance API FAQ](https://platform.claude.com/docs/en/manage-claude/compliance-faq#data-coverage-and-retention). ### Inference hooks [Inference hooks](https://platform.claude.com/docs/en/manage-claude/inference-hooks) (beta) act inline: your organization's AI security server receives each governed prompt before inference and can deny it in real time, whereas the Compliance API retrieves records after the fact and returns richer data, such as organization settings and full non-text files. *** ## In this section Enable the Compliance API for your organization, then create a Compliance Access Key (with scoped permissions) or an Admin API key, and learn which to use. Retrieve, filter, and paginate the shared Activity Feed. Supported by both key types. Read chat content, files, and project attachments; delete chats, files, and projects on demand. Compliance Access Key required. List the sessions your users run in Claude apps and agents, such as Cowork and Claude Code, and retrieve their transcripts. Compliance Access Key required. Enumerate linked organizations, members, roles, and directory groups, and read each organization's effective settings. Choose a feed-consumption pattern, plan SIEM correlation, and decide your retention approach. Every 400, 401, 403, 404, 409, 429, and 5xx response the Compliance API returns, with the fix for each. Endpoint paths, parameters, and response schemas for every Compliance API call. Answers to common key, scope, availability, and integration questions. --- title: Compliance API FAQ url: https://platform.claude.com/docs/en/manage-claude/compliance-faq description: Answers to common questions about Compliance API access, scopes, retention, and integration. --- To enable the Compliance API, see [Set up the Compliance API](https://platform.claude.com/docs/en/manage-claude/compliance-api-access). ## Access and scopes For a Claude Enterprise organization, the primary owner enables the Compliance API at [claude.ai > Organization settings > API](https://claude.ai/admin-settings/api-access), and enablement cascades from the parent organization to every linked organization. For an eligible standalone Claude Console organization (one with no parent organization), an organization admin enables it at [Claude Console > Settings > Security](https://platform.claude.com/settings/security). A Claude Console organization that is linked to a parent organization does not enable the Compliance API itself; it is enabled from the parent organization. See [Set up the Compliance API](https://platform.claude.com/docs/en/manage-claude/compliance-api-access#set-up-the-compliance-api) for the steps. Yes. For a standalone Claude Console organization, an organization admin can turn the **Compliance API** toggle off at [Claude Console > Settings > Security](https://platform.claude.com/settings/security), the same place it is turned on. While the Compliance API is off, no activity events are recorded for your organization, so the [Activity Feed](https://platform.claude.com/docs/en/manage-claude/compliance-activity-feed) receives no new events. If your organization is enrolled in [Access Transparency](https://platform.claude.com/docs/en/manage-claude/access-transparency), turning the Compliance API off also stops Access Transparency event delivery. Activity that is not recorded while the Compliance API is off cannot be recovered later. Turning the Compliance API back on resumes recording from that point forward; activity that was already recorded is not deleted. No. Turning the Compliance API off stops new activity events from being recorded, but it does not delete events that were already captured while it was on. Recording resumes from the point the Compliance API is turned back on. Yes. When the Compliance API is turned off (or back on) in Claude Console, the change is recorded as an organization settings-updated activity in the [Activity Feed](https://platform.claude.com/docs/en/manage-claude/compliance-activity-feed), so your audit trail shows who changed the setting and when. This activity is an exception to the recording stop: the disable is recorded even though no other activity is recorded while the Compliance API is off. This is expected. A Claude Enterprise parent organization centralizes identity across all linked organizations; it does not carry workloads, and it does not appear in Claude Console at all. Claude Console only ever shows the Claude Console organizations linked beneath the parent. To call the Compliance API, you create one of two key types instead: * **For full Compliance API access ([Activity Feed](https://platform.claude.com/docs/en/manage-claude/compliance-activity-feed) plus chats, files, projects, sessions, users, organization metadata, and organization settings),** the primary owner of the parent organization (or an organization owner, for a key restricted to their own organization only) creates a [Compliance Access Key](https://platform.claude.com/docs/en/manage-claude/compliance-api-access#set-up-the-compliance-api) in claude.ai. * **For Activity Feed access only,** an organization admin in your Claude Console organization creates an [Admin API key](https://platform.claude.com/docs/en/manage-claude/compliance-api-access#create-an-admin-api-key) in Claude Console. The Compliance API must already be enabled for the organization, and the admin must create the Admin API key while the Compliance API is enabled for it to carry the `read:compliance_activities` scope. No. A Claude API key (`sk-ant-api03-...`) authenticates calls to Claude models on the Claude API; it does not authenticate calls to `/v1/compliance/*`. The Compliance API accepts only Compliance Access Keys (`sk-ant-api01-...`) and Admin API keys (`sk-ant-admin01-...`). See [Which key do you need?](https://platform.claude.com/docs/en/manage-claude/compliance-api-access#which-key-do-you-need) for the full mapping. Admin API keys carry a fixed `read:compliance_activities` scope, which authorizes the Activity Feed only. Every other Compliance API endpoint requires a scope that only a Compliance Access Key created in claude.ai can carry. Calling a content or directory endpoint with an Admin API key returns a 403 naming the scope that endpoint family requires: `read:compliance_user_data` for chats, files, projects, project attachments, sessions, users, and group members, and `read:compliance_org_data` for organizations, roles, groups, and effective organization settings. For example, listing chats returns the following response. ```json Response { "error": { "type": "permission_error", "message": "Missing required scopes. Got: ['read:compliance_activities'] Needed: ['read:compliance_user_data']" } } ``` To access content endpoints, the primary owner of your parent organization (or an organization owner, for their own organization only) must [create a Compliance Access Key](https://platform.claude.com/docs/en/manage-claude/compliance-api-access#set-up-the-compliance-api) with `read:compliance_user_data` (and `delete:compliance_user_data` for deletes), or `read:compliance_org_data` for organization, role, group, and effective-settings endpoints. See [Handle Compliance API errors](https://platform.claude.com/docs/en/manage-claude/compliance-errors#403-forbidden) for the full per-endpoint catalog. ## Data coverage and retention The Activity Feed retains 6 years of organization activity, and new events are queryable within 1 minute of occurring. Activity Feed retention is independent of your organization's content retention policy: chat, file, and project content follows the retention rules configured for your organization (indefinite by default). No. The Activity Feed records who did what and when (authentication, chat creation, file uploads, project changes, administrative actions, and similar resource events), but it does not capture the prompt text or model responses inside chats or messages. To retrieve message bodies and file contents, use the chat, message, and file endpoints with a Compliance Access Key carrying `read:compliance_user_data`. The same key and scope retrieve transcripts of Cowork and Claude Code sessions on users' machines through the [local session endpoints](https://platform.claude.com/docs/en/manage-claude/compliance-sessions#retrieve-local-sessions), and transcripts of Cowork sessions in the cloud through the [remote session endpoints](https://platform.claude.com/docs/en/manage-claude/compliance-sessions#retrieve-remote-sessions). These endpoints serve Claude Enterprise content only; Claude Console workloads, and Claude API workloads authenticated with an API key, expose administrative and resource events through the Activity Feed but do not expose prompt text or model responses through the Compliance API. Yes. Cowork sessions in Claude Desktop that run on users' machines, and Claude Code sessions in the terminal, in Claude Desktop, or in an IDE extension, are captured while users are signed in with their Claude Enterprise account and are available through the [local session endpoints](https://platform.claude.com/docs/en/manage-claude/compliance-sessions#retrieve-local-sessions). Cowork sessions started on claude.ai web or mobile, which run in the cloud in Anthropic-managed environments, are available through the [remote session endpoints](https://platform.claude.com/docs/en/manage-claude/compliance-sessions#retrieve-remote-sessions). Each family has a list endpoint that returns session metadata and a messages endpoint that returns the session transcript (user prompts, assistant responses, and tool calls and results). The local family adds a third endpoint that retrieves one session's metadata. All of these endpoints use your existing Compliance Access Key with `read:compliance_user_data`; no new key or scope is needed. Local sessions are captured as their requests reach the Claude API, so nothing is installed on the device, and on-device activity that never reaches the API is not captured. Claude Code sessions authenticated with a Claude Console API key, Claude Code sessions run through a third-party cloud platform (Amazon Bedrock, Google Cloud, or Microsoft Foundry), and Claude Code on the web are not captured. Claude Code on the web also runs in the cloud in Anthropic-managed environments, but it is not a remote session; the remote session endpoints return Cowork sessions only. Organizations with [HIPAA readiness](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#hipaa-readiness) enabled get no local session data, and sessions for which [zero data retention (ZDR)](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#zero-data-retention-zdr-scope) is in effect are excluded. The local and remote session endpoints are in beta. Local and remote session transcripts both carry user prompts, assistant responses, and tool calls and results. For local sessions (Cowork and Claude Code on users' machines), that is what Claude was asked to do and what it returned, not what happened on the device. | Data | Local sessions (on users' machines) | Remote sessions (in the cloud) | | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | User prompts | Yes; returned as `text` blocks. | Yes; returned as `text` blocks. | | Assistant responses | Yes; text output only. | Yes; text output only. | | Tool calls and results | Yes; each `tool_use` input and each `text` entry in a `tool_result` is truncated to 10,000 bytes by default (up to about 1 MiB each on request). | Yes; each `tool_use` input and each `text` entry in a `tool_result` is truncated to 10,000 bytes by default (up to about 1 MiB each on request). | | File contents and file names | Yes; text that Claude reads through tools appears in the transcript, subject to the same truncation. Images, PDFs, and other binary or structured content appear only as placeholder `text` blocks. File names appear in tool-call inputs and outputs. | Yes; file contents and file names appear in the transcript through tool-call inputs and outputs (text only; other content is omitted). | | Artifacts | Yes; generated content appears inside tool-call inputs in the transcript. | Yes; generated content appears inside tool-call inputs in the transcript. | | Skills | Yes; skill content appears when the client sends it as message content, and it is not distinguished from other user text. | Yes; skill content appears in the transcript. | | Session metadata | Yes; owner (`user.id` and email address), organization, workspace, `product_surface`, and `created_at`, from the list and retrieve endpoints. Local sessions carry no `status` or `updated_at`. | Yes; owner, organization, status, timestamps, and `product_surface`, from the list endpoint. | | Thinking blocks | No. | No. | | Images and other non-text content | No; each image, PDF, or other binary or structured block appears as a placeholder `text` block (for example, `[image content not shown]`) with `truncated` set to `true`. Raw file bytes are never returned. | No; non-text blocks are omitted, and raw file bytes are never returned. | | Token usage, cost, and latency | No; use [Cowork's OpenTelemetry logging](https://support.claude.com/en/articles/14477985-monitor-claude-cowork-activity-with-opentelemetry) or [Claude Code monitoring](https://code.claude.com/docs/en/monitoring-usage) for usage and performance telemetry. | No; use [OpenTelemetry logging](https://support.claude.com/en/articles/14477985-monitor-claude-cowork-activity-with-opentelemetry) for usage and performance telemetry. | See [Sessions on users' machines](https://platform.claude.com/docs/en/manage-claude/compliance-sessions#retrieve-local-sessions) and [Sessions in the cloud](https://platform.claude.com/docs/en/manage-claude/compliance-sessions#retrieve-remote-sessions) for the endpoints and parameters. [Cowork's OpenTelemetry logging](https://support.claude.com/en/articles/14477985-monitor-claude-cowork-activity-with-opentelemetry) and [Claude Code monitoring](https://code.claude.com/docs/en/monitoring-usage) overlap with the session endpoints but answer different needs: OTEL streams per-event telemetry to infrastructure you run as activity happens, whereas the Compliance API lets you retrieve retained per-session transcripts from Anthropic after the fact. | | Local sessions (on users' machines) | Remote sessions (in the cloud) | OpenTelemetry logging | | --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | | Delivery | Pull: query and export over HTTPS | Pull: query and export over HTTPS | Push: streamed to your OTLP collector | | Setup | Works with your existing Compliance Access Key | Works with your existing Compliance Access Key | Admin configures an OTLP endpoint and content-capture settings | | Infrastructure | Anthropic-hosted | Anthropic-hosted | You run the collector and storage | | Retention | 6 years by default, or your organization's custom conversation retention period when a finite one is set; held by Anthropic | 6 years, held by Anthropic | Your infrastructure, your policies | | User prompts and assistant responses | Yes | Yes | Yes, subject to content-capture settings | | Tool inputs | Truncated to 10,000 bytes per input by default; up to about 1 MiB on request | Truncated to 10,000 bytes per input by default; up to about 1 MiB on request | Truncated summaries | | Tool result content | Each text entry truncated to 10,000 bytes by default; up to about 1 MiB on request | Each text entry truncated to 10,000 bytes by default; up to about 1 MiB on request | Metadata such as size and success; Claude Code can also capture content with an optional, size-capped setting | | File contents | Yes, through transcript tool calls (text only; other content appears as a placeholder) | Yes, through transcript tool calls (text only; other content is omitted) | File paths; Claude Code can also capture contents with an optional, size-capped setting | | Host and device metadata (terminal type, workspace paths) | No | No | Yes | | Token usage and cost | No | No | Yes | OTEL events and Compliance API records share organization and user identifiers, so you can join them. No. Deletes performed through the Compliance API are immediate, permanent, and not recoverable. Chats that a user deleted through claude.ai are soft-deleted: they remain visible through the Compliance API with `deleted_at` populated until your organization's retention window expires or you hard-delete them through this API. Pull any content you need to retain (for legal hold or archival) before issuing a `DELETE` request. The Compliance API has known coverage boundaries: the Activity Feed records resource events but not prompt or response text, Claude Console and Claude API workloads authenticated with an API key expose no message content at all, and content removed by your retention policy or by a hard delete is not recoverable. For the full coverage boundaries and delivery contract, see [Delivery guarantees and completeness](https://platform.claude.com/docs/en/manage-claude/compliance-integration-patterns#delivery-guarantees-and-completeness). Cowork and Claude Code session transcripts have boundaries of their own. Local sessions are captured only as their requests reach the Claude API, so on-device activity that never reaches the API is not captured. Claude Code sessions authenticated with a Claude Console API key, Claude Code sessions run through a third-party cloud platform (Amazon Bedrock, Google Cloud, or Microsoft Foundry), and Claude Code on the web are not captured either; organizations with HIPAA readiness enabled get no local session data; and sessions for which zero data retention is in effect are excluded. No session transcript, local or remote, includes thinking blocks or tool definitions. Organizations that use [customer-managed encryption keys](https://platform.claude.com/docs/en/manage-claude/cmek) see local session metadata but no transcript content. ## Integration and pagination Join `Activity` records to your SIEM on `actor.user_id`, `actor.email_address`, `actor.ip_address`, and `created_at`. See [Design your compliance integration](https://platform.claude.com/docs/en/manage-claude/compliance-integration-patterns#correlate-with-your-siem) for the join-key table and consumption patterns. Yes. A Claude Enterprise parent organization can have many linked organizations, including a mix of claude.ai organizations and Claude Console organizations (for example, separate production and staging Claude Console organizations). Identity, SSO, and SCIM are shared across the parent; billing, members, projects, and API keys remain separate for each organization. Compliance API enablement happens at the parent organization level and cascades to all linked organizations, and a Compliance Access Key that covers the parent organization and carries `read:compliance_org_data` can enumerate every organization beneath the parent through `GET /v1/compliance/organizations`. Activities are returned newest first, with ties in `created_at` broken by activity ID. To catch up, walk pages forward by `before_id` until `has_more` is `false`; that final response's `first_id` is your new cursor and you have reached the present. The full loop, including initial backfill and the safety conditions on cursor persistence, is in [Cursor-driven incremental reads](https://platform.claude.com/docs/en/manage-claude/compliance-integration-patterns#cursor-driven-incremental-reads). To test only the [Activity Feed](https://platform.claude.com/docs/en/manage-claude/compliance-activity-feed), you do not need a Claude Enterprise organization: an organization admin can [enable the Compliance API](https://platform.claude.com/docs/en/manage-claude/compliance-api-access#set-up-the-compliance-api) on an eligible standalone Claude Console test organization and query the feed with a new Admin API key. If the **Compliance API** section is not visible in that organization's Security settings, the organization is not eligible for self-service enablement. To test every endpoint, set up a Claude Enterprise sandbox organization linked to a Claude Console organization under the same parent. This lets the sandbox exercise both the Activity Feed (through an Admin API key) and the chat, file, project, and session endpoints (through a Compliance Access Key). 1. **Provision the Claude Enterprise organization.** Contact your Anthropic representative to set up a Claude Enterprise sandbox organization. On an existing Claude Enterprise organization, the primary owner can [enable the Compliance API directly in claude.ai](https://platform.claude.com/docs/en/manage-claude/compliance-api-access#set-up-the-compliance-api). 2. **Create the Claude Console organization.** Create a Claude Console organization yourself at `platform.claude.com` using the same email address. 3. **Link the two organizations.** Sign in as the primary owner of the Claude Enterprise organization, go to [claude.ai > Organization settings > Identity and access](https://claude.ai/admin-settings/identity), and use **Merge Organizations** to link the two under a shared parent. Once linked, follow [Set up the Compliance API](https://platform.claude.com/docs/en/manage-claude/compliance-api-access) to create keys and start querying. Test organizations use the same enablement process as production organizations. --- title: Design your compliance integration url: https://platform.claude.com/docs/en/manage-claude/compliance-integration-patterns description: Choose between polling and cursor-driven Activity Feed consumption, correlate Compliance API events with your SIEM, and plan retention. --- To enable the Compliance API, see [Set up the Compliance API](https://platform.claude.com/docs/en/manage-claude/compliance-api-access). **Required scope:** `read:compliance_activities` on the Compliance Access Key or Admin API key. A production Compliance API integration makes three design choices: how it consumes the Activity Feed, how its output correlates with your security information and event management (SIEM) system, and where long-term copies of activity and content live. These choices are independent of the endpoints themselves; this page helps you evaluate the tradeoffs. This page assumes you have read the following pages: * [Query the Activity Feed](https://platform.claude.com/docs/en/manage-claude/compliance-activity-feed), which defines the parameters and pagination contract referenced throughout. * [Retrieve and delete chats, files, and projects](https://platform.claude.com/docs/en/manage-claude/compliance-content-data), which defines the chat, file, and project endpoints and the `deleted_at` semantics referenced in [Plan content retention](https://platform.claude.com/docs/en/manage-claude/compliance-integration-patterns#plan-content-retention). * [Retrieve session transcripts](https://platform.claude.com/docs/en/manage-claude/compliance-sessions), which defines the local and remote session endpoints. ## Choose a feed-consumption pattern The Activity Feed supports two consumption patterns: periodic window polling bounded by `created_at.gte` and `created_at.lt`, and cursor-driven incremental reads that persist a cursor from one response and pass it on the next request. Both return identical `Activity` objects; the difference is the state your client persists between calls. Both patterns share these constraints: * Activities are queryable within 1 minute of occurring and retained for 6 years. * The maximum `limit` for each page is 5,000. * Cursor values are opaque strings that you must not parse. * Requests are limited to 600 per minute per [parent organization](https://platform.claude.com/docs/en/manage-claude/compliance-api#how-the-compliance-api-works), shared across every key, every linked organization, and every `/v1/compliance/*` endpoint; unlike the local session endpoints, the remote session endpoints carry a second request budget on top. See [429 Too Many Requests](https://platform.claude.com/docs/en/manage-claude/compliance-errors#429-too-many-requests) for the response headers and retry contract. | Pattern | Choose when | | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Window polling | Your pipeline runs on a fixed schedule, you prefer stateless workers, and you can tolerate replaying or overlapping windows | | Cursor-driven incremental reads | You want the lowest latency between an activity occurring and your pipeline ingesting it, you want to avoid re-reading pages you already drained, and you have a durable place to persist a cursor between runs | ### Window polling Set `created_at.lt` at least 1 minute in the past so that every activity in the window is already queryable. Use `created_at.gte` for the lower bound and `created_at.lt` for the upper bound so that consecutive windows tile without gaps or overlap; reuse the previous window's `lt` value as the next window's `gte`. ```bash cURL curl --fail-with-body -sS -G \ "https://api.anthropic.com/v1/compliance/activities" \ --header "x-api-key: $ANTHROPIC_COMPLIANCE_ACCESS_KEY" \ --data-urlencode "created_at.gte=2026-04-20T07:00:00Z" \ --data-urlencode "created_at.lt=2026-04-20T08:00:00Z" \ --data-urlencode "limit=5000" ``` When the response has `has_more: true`, the window contains more than one page of activities. Either page within the window by passing the response's `last_id` as `after_id` on the next request (stopping when `has_more` is `false`), or choose a smaller time window. See [Paginate results](https://platform.claude.com/docs/en/manage-claude/compliance-activity-feed#paginate-results) for the full contract. Even with clean tiling, an activity that indexes after its window has closed never appears in a later window. Deduplicate on the activity `id` and either widen each new window so it overlaps the previous one by a few minutes or run a periodic reconciliation pass that re-queries an older window. A `created_at.lt` bound too close to the present silently and permanently drops late-indexed activities: once `created_at.gte` advances past them, no later window can recover them. Treat the 1-minute queryability figure as the documented indexing lag, not a soft recommendation. ### Cursor-driven incremental reads ```bash cURL first_id="activity_01XyDMpzjS89pFZXqSFUBDr6" # first_id from a previous response curl --fail-with-body -sS -G \ "https://api.anthropic.com/v1/compliance/activities" \ --header "x-api-key: $ANTHROPIC_COMPLIANCE_ACCESS_KEY" \ --data-urlencode "limit=5000" \ --data-urlencode "before_id=$first_id" ``` Page through until `has_more` is `false`, then persist `first_id` from the final response and pass it unchanged as `before_id` on the next run to retrieve activities newer than the saved cursor. To walk in the opposite direction for a backfill, persist `last_id` and pass it as `after_id` instead. For the full cursor-vs-page-token reference and retry semantics, see [Paginate results](https://platform.claude.com/docs/en/manage-claude/compliance-activity-feed#paginate-results). A production **catch-up** loop fetches activities recorded since your last poll by driving iteration off `has_more` and `first_id`: ```text cursor = stored_cursor loop: page = GET /v1/compliance/activities?before_id={cursor}&limit=5000 store(page.data) if page.first_id is not null: cursor = page.first_id if not page.has_more: break persist(cursor) ``` Cursors survive key rotation; see [Manage and rotate keys](https://platform.claude.com/docs/en/manage-claude/compliance-api-access#manage-and-rotate-keys). Each page is adjacent to the cursor you pass: the loop walks forward toward the present, one page at a time. Do not treat a single response as caught up while `has_more` is `true`. Persist the cursor only after `has_more` is `false`; the unfetched pages are the newer ones between this response's `first_id` and the present, and they stay unread until you finish the loop or run again. ## Correlate with your SIEM Each `Activity` carries fields you can join against events already in your SIEM (Splunk, Datadog, Microsoft Sentinel, Cribl, or similar): | Compliance API field | Join target | | --------------------- | ----------------------------------------------- | | `actor.user_id` | Your identity provider's stable user identifier | | `actor.email_address` | Directory email when a stable ID is unavailable | | `actor.ip_address` | Network, VPN, and endpoint logs | | `created_at` | Time-window correlation across any source | `actor.user_id` and `actor.email_address` are present when `actor.type` is `user_actor`; check the discriminator before reading them. `user_id` is a stable, opaque identifier for the user account: it is consistent across every Compliance API endpoint and activity payload, and it does not change when the user's email or display name changes. Use `user_id`, not `email_address`, as the primary join key. Calls to the Compliance API itself emit `compliance_api_accessed` activities. Ingest these alongside other activity types so your SIEM records who queried compliance data, and when. Pass `activity_types[]=compliance_api_accessed` to scope the query, then in your client, read `actor.api_key_id` from each activity whose `actor.type` is `api_actor` to attribute the access to a specific Compliance Access Key or Admin API key. ## Plan content retention Five retention horizons govern what you can retrieve later: | Data | Retained for | Controlled by | | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | | Activity Feed records | 6 years | Anthropic | | Chat, file, and project content | Your organization's claude.ai retention policy | Your organization | | Local session transcripts (sessions on users' machines) | 6 years by default, or your organization's custom conversation retention period when a finite one is set | Anthropic by default; your organization when it sets a custom period | | Remote session transcripts (sessions in the cloud) | 6 years | Anthropic | | Content hard-deleted through the Compliance API | Not retained; deletion is immediate and permanent | The caller of the `DELETE` endpoint | For how the rest of the Claude Platform handles retention, see [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention). Decide between export-and-archive and on-demand API retrieval as follows: * If your legal-hold or audit horizon exceeds 6 years for activity metadata or session transcripts, export Activity Feed pages and session transcripts to your own archive as you ingest them. * If your content-retention policy is shorter than your eDiscovery horizon, export chat and file content before the retention window expires; the Compliance API cannot return content that retention has already removed. The same applies to local session transcripts, which follow your organization's custom conversation retention period when a finite one is set, even when that period is shorter than 6 years. The local session endpoints stop returning messages older than your organization's current period as soon as the setting changes, and lengthening the period later does not restore transcripts that have already expired, so export any transcript you must keep beyond it. * If a workflow might issue a Compliance API hard-delete (for example, DLP enforcement), retrieve and archive the target content first. There is no recovery window after a hard-delete; soft-deletes from claude.ai remain retrievable with `deleted_at` populated, but Compliance API deletes do not. In every other case, rely on direct API retrieval and avoid maintaining a parallel copy. ### Delivery guarantees and completeness Treat the Activity Feed as **at-least-once**: a correctly paginated traversal returns every activity at least once, but a retry after a partial failure can re-deliver activities you already stored. Deduplicate on the activity `id` field. The list endpoints do not return a `total_count` field or a checksum. To attest that an export run is complete, log: * The starting cursor and the terminal `last_id`. * The number of records exported. * The run timestamp and the `request-id` of the final page. The content endpoints (chats, files, projects, project attachments, and local and remote session transcripts) serve Claude Enterprise data only. The Activity Feed surfaces administrative and resource events organization-wide. The Compliance API does not include: * Prompt text or model responses from Claude Console, or from Claude API workloads authenticated with an API key. * On-device activity in local sessions that is never sent to Anthropic, such as local files that Claude did not read. * Claude Code usage authenticated with a Claude Console API key, run through a third-party cloud platform (Amazon Bedrock, Google Cloud, or Microsoft Foundry), or run in Claude Code on the web. * Local sessions from organizations with [HIPAA readiness](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#hipaa-readiness) enabled, and local sessions for which [zero data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention#zero-data-retention-zdr-scope) is in effect. * Thinking blocks, and images or other binary content, inside session transcripts (transcripts carry user prompts, assistant responses, and tool activity only; local session transcripts show a placeholder `text` block where binary content was omitted). * The system prompt of local sessions (a marker message stands in for it). * Tool definitions and MCP server configuration in session transcripts (local or remote), and citation metadata on `text` blocks in local session transcripts. * Transcript content for local sessions in organizations that use [customer-managed encryption keys](https://platform.claude.com/docs/en/manage-claude/cmek) (session metadata is still listed). * Content removed by your organization's retention policy. * Content hard-deleted through the Compliance API. See the [Compliance API FAQ](https://platform.claude.com/docs/en/manage-claude/compliance-faq#data-coverage-and-retention) for more on what the Compliance API does and does not capture. For chain of custody, store the exported records with provenance metadata: source endpoint, query parameters, run timestamp, and a content hash of each record. ## Next steps Filter parameters, pagination, and the `Activity` object schema. The chat, file, and project endpoints, including hard delete. List the sessions your users run in Claude apps and agents, such as Cowork and Claude Code, and retrieve their transcripts. --- title: Handle Compliance API errors url: https://platform.claude.com/docs/en/manage-claude/compliance-errors description: Every Compliance API error message with cause and fix, organized by HTTP status code. --- To enable the Compliance API, see [Set up the Compliance API](https://platform.claude.com/docs/en/manage-claude/compliance-api-access). This page lists the response messages each documented Compliance API endpoint returns, the cause, and the fix. The Compliance API returns errors in the standard [Anthropic error format](https://platform.claude.com/docs/en/api/errors): a non-2xx status code, a `request-id` response header, and a JSON body with an `error` object containing `type` and `message`. Include the `request-id` header value when you escalate to support. ```json { "error": { "type": "authentication_error", "message": "The API key provided is invalid or has been revoked." } } ``` On this page, local sessions run on users' machines and remote sessions run in the cloud; see [Retrieve session transcripts](https://platform.claude.com/docs/en/manage-claude/compliance-sessions). Match on `error.type`, not on the message string. Messages are stable enough to copy into runbooks but might be reworded over time; the type values are part of the API contract. The local session endpoints have a few documented exceptions where responses that share a type are told apart by their message; each is called out where it applies. The following table tells you at a glance whether to retry. Each section that follows shows the verbatim error body and the fix. | Status | Retry? | When | | -------------------------------------------------------------------------------------------------------------------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [400 Bad Request](https://platform.claude.com/docs/en/manage-claude/compliance-errors#400-bad-request) | No | Fix the request and resend. | | [401 Unauthorized](https://platform.claude.com/docs/en/manage-claude/compliance-errors#401-unauthorized) | No | Fix or rotate the key, then resend. | | [403 Forbidden](https://platform.claude.com/docs/en/manage-claude/compliance-errors#403-forbidden) | No | Add the missing scope or use the right key type, then resend. | | [404 Not Found](https://platform.claude.com/docs/en/manage-claude/compliance-errors#404-not-found) | Usually no | The resource was deleted or never existed; remove it from your queue. Exceptions: on the local session endpoints, the message `Local sessions are not available.` (returned on every call, including the list) means the endpoints are currently unavailable to your parent organization, not that a session is gone; keep your queued IDs and see [Local session not found](https://platform.claude.com/docs/en/manage-claude/compliance-errors#local-session-not-found). A remote session still in `pending` status 404s on its messages endpoint until it starts; see [Remote session not found](https://platform.claude.com/docs/en/manage-claude/compliance-errors#remote-session-not-found). | | [409 Conflict](https://platform.claude.com/docs/en/manage-claude/compliance-errors#409-conflict) | No | The request conflicts with the resource's current state; resolve the conflict (such as detaching child resources), then retry. | | [429 Too Many Requests](https://platform.claude.com/docs/en/manage-claude/compliance-errors#429-too-many-requests) | Yes, after `retry-after` | Wait the seconds in `retry-after`, then retry; do not advance your cursor. | | [500 Internal Server Error](https://platform.claude.com/docs/en/manage-claude/compliance-errors#500-internal-server-error) | Depends on `x-should-retry` | Check the `x-should-retry` response header before retrying. | | [502, 503, 504, 529](https://platform.claude.com/docs/en/manage-claude/compliance-errors#500-internal-server-error) | Yes, with backoff | Transient; retry with exponential backoff. Exception: one local session 503 is data-dependent and can persist; see [Local sessions temporarily unavailable](https://platform.claude.com/docs/en/manage-claude/compliance-errors#local-sessions-temporarily-unavailable). | ## 400 Bad Request The request was syntactically valid but contained a parameter the server rejected. Fix the parameter and retry. ### Invalid timestamp format **Type:** `invalid_request_error` ```text wrap The `created_at.gte` parameter contains an invalid timestamp format. Timestamps must be provided in RFC 3339 format e.g., "2024-03-01T00:00:00Z". Got "2024-01-01". ``` **Cause:** A `created_at.*` or `updated_at.*` value (`.gte`, `.gt`, `.lte`, `.lt`) could not be parsed as a datetime. The message names the parameter that failed and echoes the value that was sent. **Fix:** Send a full RFC 3339 timestamp including time and time zone, for example, `2024-03-01T00:00:00Z` or `2024-03-01T00:00:00+00:00`. The local session list (`GET /v1/compliance/apps/sessions/local`) also returns a 400 `invalid_request_error` when both time bounds are supplied and `created_at.lt` is not strictly after `created_at.gte`. The body reads: ```text wrap created_at.lt must be strictly after created_at.gte. ``` Send a `created_at.lt` later than `created_at.gte`, or omit one of the bounds. ### Invalid limit **Type:** `invalid_request_error` ```text wrap The limit parameter must be between 1 and 1000, inclusive. Got 1500. ``` **Cause:** The `limit` query parameter was outside the accepted range. The bound named in the message reflects the maximum for the specific endpoint that was called. **Fix:** Send a `limit` within the range the endpoint accepts. Each list endpoint has its own `limit` range; see the parameter constraints on the corresponding [Compliance API reference](https://platform.claude.com/docs/en/api/compliance) page. The session transcript endpoints (`GET /v1/compliance/apps/sessions/local/{session_id}/messages` and `GET /v1/compliance/apps/sessions/remote/{session_id}/messages`) validate their truncation parameters the same way: `tool_use_input_max_bytes` and `tool_result_max_bytes` each accept a positive byte count or `-1` (the server maximum), so a value such as `0` returns the same 400 `invalid_request_error`. ### Invalid pagination ID **Type:** `invalid_request_error` ```text wrap Invalid `after_id`. No activity found for `after_id` "activity_invalid123" ``` **Cause:** The `after_id` or `before_id` cursor could not be decoded as an opaque cursor or parsed as an activity ID. **Fix:** Treat pagination cursors as opaque strings. Always copy the `first_id` or `last_id` value returned by the previous page; stop when `has_more` is `false`. Do not construct cursors from object IDs. The directory, project, and session endpoints (organizations, users, roles, role permissions, groups, group members, projects, project attachments, local and remote sessions, and session messages) paginate with an opaque `page` token rather than `after_id` and `before_id`. The same advice applies: pass the `next_page` value from the previous response unchanged, and stop when `has_more` is `false` (or, on the session endpoints, which return no `has_more`, when `next_page` is `null`). A malformed `page` token returns the same 400 `invalid_request_error` as a malformed `after_id` or `before_id`. The two paginated local session endpoints (the list and the messages endpoint) return the following 400 `invalid_request_error` for any `page` value they cannot decode, for example a token that was truncated or altered after you stored it, or one issued by a different endpoint or under a different parent organization. On the local session messages endpoint (`GET /v1/compliance/apps/sessions/local/{session_id}/messages`), each `page` cursor is also bound to the session and `order` it was issued for, so a cursor issued for a different session or sort order returns the same body: ```text wrap The page parameter is not a valid cursor for this request. ``` Cursors on the messages endpoint also expire 24 hours after the walk (one pass through the pages) began. An expired cursor returns: ```text wrap The page cursor has expired. Restart the walk without a page parameter; results will reflect the current retention boundary. ``` For the first body, resend the unmodified `next_page` value from the previous response to the endpoint and session that issued it. For an expired cursor, restart without a `page` parameter; the new walk reflects the retention boundary in effect when it starts, so messages that aged out of the retention period in the meantime are no longer returned (see [Retrieve a local session transcript](https://platform.claude.com/docs/en/manage-claude/compliance-sessions#retrieve-a-local-session-transcript)). ## 401 Unauthorized The `x-api-key` header was missing or did not match a known key. A valid key with the wrong scopes returns [403 Forbidden](https://platform.claude.com/docs/en/manage-claude/compliance-errors#403-forbidden) instead. ### Invalid API key **Type:** `authentication_error` ```text wrap The API key provided is invalid or has been revoked. ``` **Cause:** The key in `x-api-key` does not exist, has been deleted, or has been disabled. A missing or empty `x-api-key` header returns the same body, so check both your secret store and the key's revocation status. **Fix:** Confirm the key value, check that it has not been deleted in claude.ai (Compliance Access Keys) or Claude Console (Admin API keys), and confirm it is enabled. See [Set up the Compliance API](https://platform.claude.com/docs/en/manage-claude/compliance-api-access). ## 403 Forbidden The key in `x-api-key` is valid but does not carry the scope the endpoint requires. The verbatim message lists the scopes the key carries (`Got:`) and the scopes the endpoint requires (`Needed:`), so you can confirm what the key carries without rechecking Claude Console or claude.ai. Compliance Access Key scopes are immutable after creation, so each insufficient-scope fix directs you to create a new key rather than edit the existing one. ### Insufficient scope: Activity Feed **Type:** `permission_error` ```text wrap Missing required scopes. Got: ['read:compliance_user_data'] Needed: ['read:compliance_activities'] ``` **Cause:** A key without `read:compliance_activities` was used to call `GET /v1/compliance/activities`. There are two common paths to this error: * A Compliance Access Key (`sk-ant-api01-...`) was created without the `read:compliance_activities` scope. * A Claude Console Admin API key (`sk-ant-admin01-...`) was created while the Compliance API was not enabled for the organization. Keys created while the Compliance API was not enabled do not carry the scope; see [Set up the Compliance API](https://platform.claude.com/docs/en/manage-claude/compliance-api-access#set-up-the-compliance-api). **Fix:** Compliance Access Key scopes are immutable after creation. Create a new key that includes `read:compliance_activities`, or use a Claude Console Admin API key. See [Which key do you need?](https://platform.claude.com/docs/en/manage-claude/compliance-api-access#which-key-do-you-need) for the conditions under which an Admin API key carries this scope. ### Insufficient scope: organization data **Type:** `permission_error` ```text wrap Missing required scopes. Got: ['read:compliance_user_data'] Needed: ['read:compliance_org_data'] ``` **Cause:** A key without `read:compliance_org_data` was used to call an organizations, roles, groups, or effective-settings endpoint. There are two common paths to this error: * A Compliance Access Key (`sk-ant-api01-...`) was created without the `read:compliance_org_data` scope. * A Claude Console Admin API key (`sk-ant-admin01-...`) was used. Admin API keys carry only `read:compliance_activities` and cannot read organization metadata. **Fix:** [Create a new Compliance Access Key](https://platform.claude.com/docs/en/manage-claude/compliance-api-access#set-up-the-compliance-api) with `read:compliance_org_data` selected. Admin API keys cannot read organization metadata; the Compliance Access Key is required. ### Retired scope: organization settings **Type:** `permission_error` ```text wrap Missing required scopes. Got: ['read:compliance_org_settings'] Needed: ['read:compliance_org_data'] ``` **Cause:** The `read:compliance_org_settings` scope was retired on June 30, 2026. `GET /v1/compliance/organizations/{organization_id}/settings` now requires `read:compliance_org_data`, the same scope as the other organization endpoints, and the retired scope no longer authorizes anything. A Compliance Access Key that carries only `read:compliance_org_settings` returns this error on every call to the settings endpoint, even though the key worked before the retirement. The retired scope can no longer be selected or granted when creating a key. **Fix:** Compliance Access Key scopes are immutable after creation. [Create a new Compliance Access Key](https://platform.claude.com/docs/en/manage-claude/compliance-api-access#set-up-the-compliance-api) with `read:compliance_org_data` selected, update your integration to use it, then delete the old key. A key that already carries `read:compliance_org_data` is unaffected by the retirement. ### Insufficient scope: user data **Type:** `permission_error` ```text wrap Missing required scopes. Got: ['read:compliance_activities'] Needed: ['read:compliance_user_data'] ``` **Cause:** A key without `read:compliance_user_data` was used to call a chats, messages, files,