ADR-0011: Anthropic Skills API Integration
View SourceStatus
Proposed
Context
Note: This ADR replaces an earlier incorrect design that assumed a "pass-through executor" pattern. The original design was based on a misunderstanding of how Anthropic's code execution capabilities work. This revision accurately describes the actual Anthropic Skills API.
Anthropic provides a Skills API (beta) that allows:
- Uploading custom skills to Anthropic's infrastructure
- Using pre-built Anthropic skills (xlsx, pptx, docx, pdf)
- Executing skills in Anthropic-managed containers with code execution
This provides an alternative to local/Docker execution for users who:
- Cannot run Docker in their environment
- Want Anthropic to manage sandbox security
- Need access to Anthropic's pre-built document skills
- Prefer hosted execution without local infrastructure
How the Skills API Actually Works
1. Upload skill files → POST /v1/skills → receive skill_id
2. Include skill_id in container.skills parameter
3. Enable code_execution_20250825 tool in the request
4. Anthropic's container loads skills at /skills/{directory}/
5. Claude uses code execution to run code with skill files available
6. Download created files via Files APIKey Differences from Original Design
| Original (Incorrect) | Actual API |
|---|---|
| Pass-through for bash commands | Skills uploaded first, get skill_id |
{:passthrough, config} return | Uses container.skills parameter |
| Executor behaviour implementation | Not an executor—API integration helpers |
| Local skill files | Skills must be uploaded to Anthropic |
Beta Requirements
The Skills API requires beta headers:
code-execution-2025-08-25- Enables code executionskills-2025-10-02- Enables Skills APIfiles-api-2025-04-14- For uploading/downloading files
Skill Types
| Type | Description | ID Format |
|---|---|---|
anthropic | Pre-built by Anthropic | Short names: xlsx, pptx, docx, pdf |
custom | User-uploaded | Generated: skill_01AbCdEfGhIjKlMnOpQrStUv |
Decision
We will provide optional Anthropic Skills API integration as helper modules, NOT as an executor implementation. This is fundamentally different from local/Docker execution because:
- Conjure does NOT execute tools—Anthropic's container does
- Skills must be uploaded to Anthropic first
- The integration is at the API request level, not execution level
Note: While this ADR describes the Anthropic-specific modules, ADR-0019: Unified Execution Model describes how these modules are surfaced through a unified
Conjure.SessionAPI that provides identical interaction patterns for both local/Docker and Anthropic execution.
Module Design
defmodule Conjure.Skills.Anthropic do
@moduledoc """
Upload and manage skills via Anthropic Skills API.
This module provides helpers for interacting with Anthropic's
Skills API to upload custom skills and manage versions.
Note: This is NOT an executor. Skills uploaded here are executed
by Anthropic's infrastructure, not by Conjure.
"""
@doc """
Upload a skill directory to Anthropic.
Returns the skill_id for use in API requests.
## Example
{:ok, skill_id} = Conjure.Skills.Anthropic.upload(
"priv/skills/csv-helper",
display_title: "CSV Helper",
api_key: System.get_env("ANTHROPIC_API_KEY")
)
"""
@spec upload(Path.t(), keyword()) :: {:ok, String.t()} | {:error, term()}
def upload(skill_path, opts \\ [])
@doc """
List skills available in your Anthropic workspace.
## Options
* `:source` - Filter by "anthropic" or "custom"
* `:api_key` - Anthropic API key
"""
@spec list(keyword()) :: {:ok, [map()]} | {:error, term()}
def list(opts \\ [])
@doc """
Delete a custom skill from Anthropic.
Note: All versions must be deleted first.
"""
@spec delete(String.t(), keyword()) :: :ok | {:error, term()}
def delete(skill_id, opts \\ [])
@doc """
Create a new version of an existing skill.
"""
@spec create_version(String.t(), Path.t(), keyword()) :: {:ok, String.t()} | {:error, term()}
def create_version(skill_id, skill_path, opts \\ [])
endAPI Request Helpers
defmodule Conjure.API.Anthropic do
@moduledoc """
Helpers for building Anthropic API requests with Skills.
"""
@doc """
Build the container parameter for skills.
## Example
container = Conjure.API.Anthropic.container_config([
{:anthropic, "xlsx", "latest"},
{:anthropic, "pptx", "latest"},
{:custom, "skill_01AbCdEfGhIjKlMnOpQrStUv", "latest"}
])
# Returns:
# %{
# "skills" => [
# %{"type" => "anthropic", "skill_id" => "xlsx", "version" => "latest"},
# %{"type" => "anthropic", "skill_id" => "pptx", "version" => "latest"},
# %{"type" => "custom", "skill_id" => "skill_01...", "version" => "latest"}
# ]
# }
"""
@spec container_config([skill_spec()]) :: map()
def container_config(skills)
@type skill_spec ::
{:anthropic, String.t(), String.t()} |
{:custom, String.t(), String.t()}
@doc """
Get the required beta headers for Skills API.
"""
@spec beta_headers() :: [{String.t(), String.t()}]
def beta_headers do
[
{"anthropic-beta", "code-execution-2025-08-25,skills-2025-10-02,files-api-2025-04-14"}
]
end
@doc """
Get the code execution tool definition.
"""
@spec code_execution_tool() :: map()
def code_execution_tool do
%{
"type" => "code_execution_20250825",
"name" => "code_execution"
}
end
endMulti-Skill Support
The Skills API supports up to 8 skills per request. Skills can be combined for complex workflows (e.g., analyze data with Excel skill, create presentation with PowerPoint skill):
container = Conjure.API.Anthropic.container_config([
{:anthropic, "xlsx", "latest"},
{:anthropic, "pptx", "latest"},
{:anthropic, "pdf", "latest"},
{:custom, "skill_01AbCdEfGhIjKlMnOpQrStUv", "latest"}
])Long-Running Operations
Skills may perform operations that require multiple turns. The API returns a pause_turn stop reason when an operation is paused, requiring the client to continue the conversation:
defmodule Conjure.Conversation.Anthropic do
@moduledoc """
Conversation loop for Anthropic Skills API with pause_turn handling.
"""
@doc """
Run a conversation with Anthropic-hosted skills, handling pause_turn.
Unlike local/Docker execution where Conjure manages tool execution,
here Anthropic executes in their container. However, long-running
operations still require a conversation loop to handle pause_turn.
## Options
* `:max_retries` - Maximum pause_turn iterations (default: 10)
* `:on_pause` - Callback when pause_turn received
"""
@spec run(list(), map(), keyword()) :: {:ok, map()} | {:error, term()}
def run(messages, container_config, opts \\ []) do
max_retries = Keyword.get(opts, :max_retries, 10)
do_run(messages, container_config, opts, 0, max_retries)
end
defp do_run(messages, container_config, opts, attempt, max_retries)
when attempt < max_retries do
case call_api(messages, container_config, opts) do
{:ok, %{"stop_reason" => "pause_turn", "content" => content} = response} ->
# Long-running operation paused - continue with same container
container_id = get_in(response, ["container", "id"])
updated_messages = messages ++ [%{role: "assistant", content: content}]
updated_container = Map.put(container_config, "id", container_id)
if callback = opts[:on_pause] do
callback.(response, attempt + 1)
end
do_run(updated_messages, updated_container, opts, attempt + 1, max_retries)
{:ok, response} ->
{:ok, response}
{:error, reason} ->
{:error, reason}
end
end
defp do_run(_messages, _container, _opts, attempt, max_retries) do
{:error, {:max_retries_exceeded, attempt, max_retries}}
end
endMulti-Turn Conversations
Reuse the same container across multiple user messages by preserving the container ID:
defmodule Conjure.Session.Anthropic do
@moduledoc """
Manage multi-turn sessions with Anthropic Skills API.
"""
defstruct [:container_id, :skills, :messages]
@doc """
Start a new session with specified skills.
"""
def new(skills) do
%__MODULE__{
container_id: nil,
skills: skills,
messages: []
}
end
@doc """
Send a message and get response, preserving container state.
"""
def chat(session, user_message, opts \\ []) do
messages = session.messages ++ [%{role: "user", content: user_message}]
container_config = build_container(session)
case Conjure.Conversation.Anthropic.run(messages, container_config, opts) do
{:ok, response} ->
updated_session = %{session |
container_id: get_in(response, ["container", "id"]),
messages: messages ++ [%{role: "assistant", content: response["content"]}]
}
{:ok, response, updated_session}
{:error, reason} ->
{:error, reason}
end
end
defp build_container(%{container_id: nil, skills: skills}) do
Conjure.API.Anthropic.container_config(skills)
end
defp build_container(%{container_id: id, skills: skills}) do
Conjure.API.Anthropic.container_config(skills)
|> Map.put("id", id)
end
endFile Handling
Skills that create documents return file_id values. Use the Files API to download:
defmodule Conjure.Files.Anthropic do
@moduledoc """
Download files created by Anthropic Skills.
"""
@doc """
Extract file IDs from a response.
"""
@spec extract_file_ids(map()) :: [String.t()]
def extract_file_ids(response)
@doc """
Download a file by ID.
"""
@spec download(String.t(), keyword()) :: {:ok, binary(), String.t()} | {:error, term()}
def download(file_id, opts \\ [])
@doc """
Get file metadata.
"""
@spec metadata(String.t(), keyword()) :: {:ok, map()} | {:error, term()}
def metadata(file_id, opts \\ [])
endUsage Example
defmodule MyApp.AnthropicSkillChat do
@moduledoc """
Example: Using Anthropic's hosted skills with full conversation support.
"""
alias Conjure.Session.Anthropic, as: Session
alias Conjure.Conversation.Anthropic, as: Conversation
def chat_with_hosted_skills(user_message) do
# Configure skills (up to 8)
skills = [
{:anthropic, "xlsx", "latest"},
{:anthropic, "pptx", "latest"}
]
# Start session
session = Session.new(skills)
# Chat with pause_turn handling for long-running operations
case Session.chat(session, user_message, on_pause: &log_pause/2) do
{:ok, response, updated_session} ->
# Download any created files
file_ids = Conjure.Files.Anthropic.extract_file_ids(response)
files = Enum.map(file_ids, &download_file/1)
{:ok, response, files, updated_session}
{:error, reason} ->
{:error, reason}
end
end
defp log_pause(response, attempt) do
IO.puts("Operation paused (attempt #{attempt}), continuing...")
end
defp download_file(file_id) do
{:ok, content, filename} = Conjure.Files.Anthropic.download(file_id)
File.write!(filename, content)
filename
end
endComparison: Local/Docker vs Anthropic Hosted
| Aspect | Local/Docker | Anthropic Hosted |
|---|---|---|
| Who executes | Your application | Anthropic's container |
| Skill location | Local filesystem | Uploaded to Anthropic |
| Conversation loop | Tool call/result loop | pause_turn handling loop |
| Multi-turn state | Message history | Container ID + messages |
| Skills per request | Unlimited | Up to 8 |
| Network from sandbox | Configurable | Never (isolated) |
| Pre-built skills | N/A | xlsx, pptx, docx, pdf |
| File output | Local filesystem | Files API download |
| Cost | Your infrastructure | Anthropic API pricing |
Consequences
Positive
- No local Docker required - Anthropic manages containers
- Pre-built document skills - Access xlsx, pptx, docx, pdf without custom implementation
- Multi-skill workflows - Combine up to 8 skills per request
- Long-running support - pause_turn handling for complex operations
- Persistent sessions - Container ID reuse for multi-turn conversations
- Anthropic-managed security - Sandbox isolation handled by Anthropic
- Consistent environment - Same execution environment for all users
- File output support - Files API for downloading created documents
Negative
- Beta feature - API may change, requires beta headers
- Skills must be uploaded - Cannot use local skill files directly
- Network dependency - Requires connectivity to Anthropic API
- No network from container - Skills cannot make external API calls
- No runtime package installation - Only pre-installed packages available
- Upload size limit - 8MB maximum per skill
- Skills limit - Maximum 8 skills per request
- Conversation loop required - Must handle pause_turn for long operations
Neutral
- Different architecture - Not an executor, but API integration
- Complementary to local - Can use both approaches in same application
- Version management - Skills have versions, can pin for stability
- Two loop types - Local uses tool call/result loop, hosted uses pause_turn loop
Alternatives Considered
Executor Behaviour Implementation (Original Design)
The original ADR proposed implementing Conjure.Executor.Anthropic as a behaviour that returns {:passthrough, config}. This was rejected because:
- Based on incorrect understanding of the API
- Anthropic doesn't accept arbitrary bash commands
- Skills must be uploaded first, not passed through
- The conversation loop model doesn't apply—it's a single request
Transparent Upload on Execute
Automatically upload skills when first used. Rejected because:
- Requires API key in executor context
- Upload is a separate concern from execution
- Better to make upload explicit for version control
- Skills have IDs that should be managed explicitly
Skip Anthropic Integration Entirely
Only support local/Docker execution. Rejected because:
- Users may want hosted execution without Docker
- Pre-built document skills are valuable
- Anthropic's security expertise for sandboxing
- Useful for environments where Docker isn't available