Getting Started with Claude Agent SDK for Elixir

Copy Markdown View Source

Welcome to the Claude Agent SDK for Elixir! This guide will walk you through setting up your environment, making your first query, and understanding the core patterns you will use throughout your development.

The Claude Agent SDK provides a production-ready Elixir interface to Claude Code, enabling you to build AI-powered tools and automate coding workflows with Claude's capabilities.


Table of Contents

  1. Prerequisites
  2. Installation
  3. Your First Query
  4. Understanding the Response Stream
  5. Common Patterns
  6. Next Steps

Prerequisites

Before you begin, ensure you have the following installed and configured.

1. Elixir and Erlang

The SDK requires Elixir 1.14 or later. Check your version:

elixir --version

If you need to install or upgrade Elixir, visit elixir-lang.org/install or use a version manager like asdf:

asdf install elixir 1.14.5
asdf global elixir 1.14.5

2. Claude Code CLI

The SDK communicates with Claude through the Claude Code CLI. Install it globally via npm:

npm install -g @anthropic-ai/claude-code

Verify the installation:

claude --version

You should see a version number like 2.1.12 or higher.

3. Authentication

You need valid credentials to use Claude. Choose one of these authentication methods:

Option A: Interactive Login (Recommended for Development)

Run the login command and follow the prompts:

claude login

This opens a browser for OAuth authentication and stores your credentials locally.

Option B: API Key (Recommended for Production)

Set the ANTHROPIC_API_KEY environment variable:

export ANTHROPIC_API_KEY="sk-ant-api03-..."

Add this to your shell profile (.bashrc, .zshrc, etc.) for persistence.

Option C: OAuth Token (Programmatic Access)

Set the CLAUDE_AGENT_OAUTH_TOKEN environment variable:

export CLAUDE_AGENT_OAUTH_TOKEN="your-oauth-token"

Option D: SDK Token Setup Task (Interactive)

You can also generate and persist an OAuth token with the SDK task:

mix claude.setup_token

To clear stored auth:

case ClaudeAgentSDK.AuthManager.clear_auth() do
  :ok -> IO.puts("Auth cleared")
  {:error, reason} -> IO.puts("Failed to clear auth: #{inspect(reason)}")
end

Verify Your Setup

Run a quick test to ensure everything is working:

claude -p "Say hello"

If you see a response from Claude, you are ready to proceed.


Installation

Add claude_agent_sdk to your Mix dependencies in mix.exs:

defp deps do
  [
    {:claude_agent_sdk, "~> 0.11.0"}
  ]
end

Then fetch the dependency:

mix deps.get

Compile the Project

mix compile

The SDK is now ready to use in your application.


Your First Query

Let us create a simple example that sends a prompt to Claude and displays the response.

Minimal Example

Create a file called hello_claude.exs:

# hello_claude.exs

# Simple query using defaults
ClaudeAgentSDK.query("Say hello in exactly five words.")
|> Enum.each(fn message ->
  case message.type do
    :assistant ->
      IO.puts("Claude says: #{message.data.message["content"]}")

    :result ->
      IO.puts("\nCost: $#{message.data.total_cost_usd}")
      IO.puts("Turns: #{message.data.num_turns}")

    _ ->
      :ok
  end
end)

Run it:

mix run hello_claude.exs

You should see output similar to:

Claude says: Hello there, nice to meet!

Cost: $0.00025
Turns: 1

Using Options

For more control, use the Options struct:

# hello_with_options.exs

alias ClaudeAgentSDK.Options

# Configure the query
options = %Options{
  model: "haiku",           # Use Claude Haiku for fast responses
  max_turns: 3,             # Limit conversation turns
  system_prompt: "You are a friendly assistant who speaks concisely."
}

# Run the query
ClaudeAgentSDK.query("What is Elixir best known for?", options)
|> Enum.each(fn message ->
  case message.type do
    :assistant ->
      IO.puts(message.data.message["content"])

    :result ->
      IO.puts("\n---")
      IO.puts("Duration: #{message.data.duration_ms}ms")

    _ ->
      :ok
  end
end)

For advanced transport lifecycle control, query supports transport injection:

ClaudeAgentSDK.query(
  "Hello",
  options,
  {ClaudeAgentSDK.Transport.Port, [startup_mode: :lazy]}
)
|> Enum.to_list()

Using the OptionBuilder

For common configurations, use the pre-built option presets:

alias ClaudeAgentSDK.{OptionBuilder, ContentExtractor}

# Use Haiku model preset
options = OptionBuilder.with_haiku()

# Query and extract text
response = ClaudeAgentSDK.query("Explain pattern matching in one sentence.", options)
|> Enum.to_list()

# Extract just the text content
text = response
|> Enum.filter(&(&1.type == :assistant))
|> Enum.map(&ContentExtractor.extract_text/1)
|> Enum.join("\n")

IO.puts(text)

Understanding the Response Stream

The ClaudeAgentSDK.query/2 function returns a lazy stream of messages. This design provides:

  • Memory efficiency: Messages are processed one at a time
  • Real-time output: Display responses as they arrive
  • Flexibility: Filter, transform, or collect as needed

Message Types

Each message in the stream has a type field indicating what it represents:

TypeDescriptionKey Data Fields
:systemSession initializationsession_id, model, cwd, tools
:userUser input echomessage, session_id
:assistantClaude's responsemessage (with content), session_id
:resultFinal summarytotal_cost_usd, duration_ms, num_turns
:stream_eventStreaming eventevent, uuid, session_id, parent_tool_use_id

Note: :stream_event messages expose the raw CLI event in data.event. For parsed streaming events (with :text_delta, :message_start, etc.), use the Streaming API. Parsed events include uuid, session_id, parent_tool_use_id, and raw_event.

Note: CLI JSON frames are capped by max_buffer_size (default 1MB). If a frame exceeds the limit, the stream terminates with a CLIJSONDecodeError result.

Example: Processing Each Message Type

alias ClaudeAgentSDK.Options

options = %Options{model: "haiku", max_turns: 1}

ClaudeAgentSDK.query("Write a haiku about coding.", options)
|> Enum.each(fn message ->
  case message.type do
    :system ->
      IO.puts("[Session started: #{message.data.session_id}]")
      IO.puts("[Model: #{message.data.model}]")

    :user ->
      IO.puts("\n> #{message.data.message}")

    :assistant ->
      IO.puts("\nClaude:")
      IO.puts(message.data.message["content"])

    :result ->
      IO.puts("\n---")
      IO.puts("Completed in #{message.data.duration_ms}ms")
      IO.puts("Cost: $#{Float.round(message.data.total_cost_usd, 6)}")

    _ ->
      :ok
  end
end)

Result Subtypes

The :result message includes a subtype field indicating how the session ended:

SubtypeDescription
:successNormal completion
:error_max_turnsMax turns limit reached
:error_during_executionError occurred during execution

Check for errors:

result = Enum.find(messages, &(&1.type == :result))

case result.subtype do
  :success ->
    IO.puts("Query completed successfully!")

  :error_max_turns ->
    IO.puts("Warning: Reached maximum turns limit")

  :error_during_execution ->
    IO.puts("Error: #{result.data.error}")
end

Common Patterns

These patterns will help you write clean, maintainable code with the SDK.

Pattern 1: Extract Text from Response

The most common need is extracting the text content from Claude's response:

alias ClaudeAgentSDK.{OptionBuilder, ContentExtractor}

def get_claude_response(prompt) do
  OptionBuilder.with_haiku()
  |> then(&ClaudeAgentSDK.query(prompt, &1))
  |> Enum.to_list()
  |> Enum.filter(&(&1.type == :assistant))
  |> Enum.map(&ContentExtractor.extract_text/1)
  |> Enum.join("\n")
end

# Usage
response = get_claude_response("What is OTP?")
IO.puts(response)

Pattern 2: Handle Errors Gracefully

Always handle potential errors in your queries:

alias ClaudeAgentSDK.{Options, Message}

def safe_query(prompt, options \\ %Options{}) do
  try do
    messages = ClaudeAgentSDK.query(prompt, options) |> Enum.to_list()

    # Check for errors in assistant messages
    error = Enum.find_value(messages, fn
      %Message{type: :assistant, data: %{error: err}} when not is_nil(err) -> err
      _ -> nil
    end)

    if error do
      {:error, error}
    else
      # Check result subtype
      result = Enum.find(messages, &(&1.type == :result))

      case result do
        %{subtype: :success} ->
          {:ok, messages}

        %{subtype: :error_during_execution, data: %{error: err}} ->
          {:error, err}

        %{subtype: :error_max_turns} ->
          {:error, :max_turns_reached}

        _ ->
          {:ok, messages}
      end
    end
  rescue
    e -> {:error, Exception.message(e)}
  end
end

# Usage
case safe_query("Hello, Claude!") do
  {:ok, messages} ->
    IO.puts("Success!")

  {:error, reason} ->
    IO.puts("Failed: #{inspect(reason)}")
end

Pattern 3: Collect Session Metadata

Extract useful metadata from the response:

alias ClaudeAgentSDK.Session

def query_with_metadata(prompt, options) do
  messages = ClaudeAgentSDK.query(prompt, options) |> Enum.to_list()

  %{
    session_id: Session.extract_session_id(messages),
    model: Session.extract_model(messages),
    cost: Session.calculate_cost(messages),
    turns: Session.count_turns(messages),
    messages: messages
  }
end

Pattern 4: Resume Conversations

Continue a previous conversation using the session ID:

alias ClaudeAgentSDK.{Options, Session}

# First query
options = %Options{model: "haiku", max_turns: 5}
messages1 = ClaudeAgentSDK.query("My name is Alice.", options) |> Enum.to_list()

# Extract session ID
session_id = Session.extract_session_id(messages1)
IO.puts("Session: #{session_id}")

# Resume with follow-up
messages2 = ClaudeAgentSDK.resume(session_id, "What is my name?", options)
|> Enum.to_list()

# Claude remembers: "Your name is Alice."

Pattern 5: Stream with Typewriter Effect

Display responses in real-time as Claude generates them:

alias ClaudeAgentSDK.{Options, Streaming}

options = %Options{model: "haiku", max_turns: 1}

{:ok, session} = Streaming.start_session(options)

try do
  Streaming.send_message(session, "Tell me a short joke.")
  |> Enum.each(fn event ->
    case event do
      %{type: :text_delta, text: chunk} ->
        IO.write(chunk)

      %{type: :message_stop} ->
        IO.puts("\n")

      %{type: :error, error: reason} ->
        IO.puts("\nError: #{inspect(reason)}")

      _ ->
        :ok
    end
  end)
after
  Streaming.close_session(session)
end

Next Steps

Now that you have the basics, explore these areas to deepen your knowledge:

Guides and Documentation

Examples

The examples/ directory contains runnable demonstrations of all SDK features:

# Run a single example
mix run examples/basic_example.exs

# Run all examples
bash examples/run_all.sh

Key examples to explore:

ExampleDescription
basic_example.exsMinimal query pattern
session_features_example.exsSession persistence and resume
sdk_mcp_tools_live.exsDefine in-process tools with deftool
streaming_tools/quick_demo.exsReal-time streaming
hooks/basic_bash_blocking.exsSecurity hooks for tool control

Advanced Features

Once comfortable with the basics, explore:

  1. SDK MCP Tools - Define custom tools that Claude can invoke:

    defmodule MyTools do
      use ClaudeAgentSDK.Tool
    
      deftool :add, "Add two numbers", %{
        type: "object",
        properties: %{a: %{type: "number"}, b: %{type: "number"}},
        required: ["a", "b"]
      } do
        def execute(%{"a" => a, "b" => b}) do
          {:ok, %{"content" => [%{"type" => "text", "text" => "#{a + b}"}]}}
        end
      end
    end
  2. Hooks - Intercept and control Claude's actions:

    alias ClaudeAgentSDK.Hooks.{Matcher, Output}
    
    hooks = %{
      pre_tool_use: [
        Matcher.new("Bash", [fn input, _id, _ctx ->
          # Block dangerous commands
          if String.contains?(input["tool_input"]["command"], "rm -rf") do
            Output.deny("Dangerous command blocked")
          else
            Output.allow()
          end
        end])
      ]
    }
    
    options = %Options{hooks: hooks}
  3. Permissions - Fine-grained tool access control:

    alias ClaudeAgentSDK.Permission.Result
    
    callback = fn context ->
      case context.tool_name do
        "Write" -> Result.allow()
        "Bash" -> Result.deny("Bash disabled")
        _ -> Result.allow()
      end
    end
    
    options = %Options{
      can_use_tool: callback,
      permission_mode: :default
    }

    can_use_tool routes query/2 through the control client (string or streaming prompts), enables include_partial_messages, and is mutually exclusive with permission_prompt_tool (the SDK sets it to "stdio" internally). Use :default for built-in tool permissions.

Getting Help


Summary

You have learned how to:

  1. Set up your environment with Elixir, Claude CLI, and authentication
  2. Install the Claude Agent SDK via Mix
  3. Make basic queries and understand the response stream
  4. Use common patterns for text extraction, error handling, and streaming
  5. Find resources for continued learning

The Claude Agent SDK opens up powerful possibilities for AI-assisted development. Start with simple queries, then gradually explore hooks, tools, and advanced patterns as your needs grow.

Happy coding with Claude!