docs/tutorials/using_elixir_native_skill.md

# Native Elixir Skills: Type-Safe In-Process Execution

Build skills as Elixir modules that execute directly in the BEAM with full access to your application.

**Time:** 25 minutes

**Prerequisites:** Complete [Hello World](hello_world.md) first.

## What You'll Build

1. A pure Elixir Echo skill (simple)
2. A Log Fetcher skill that calls REST APIs from Elixir (production)
3. Tests for native skills

## Why Native Skills?

| Aspect | Local Skills | Native Skills |
|--------|--------------|---------------|
| Language | Python, Bash, etc. | Elixir |
| Execution | Subprocess/shell | Direct function call |
| Overhead | Process spawn | None |
| Type Safety | Runtime errors | Compile-time checks |
| App Access | None | Full (Ecto, GenServers, etc.) |

Use native skills when you need:
- Direct access to your Elixir application state
- Faster execution without shell overhead
- Type-safe, pattern-matched implementations
- Integration with Ecto, Phoenix, GenServers

## The NativeSkill Behaviour

Native skills implement `Conjure.NativeSkill`:

```elixir
defmodule Conjure.NativeSkill do
  @callback __skill_info__() :: %{
    name: String.t(),
    description: String.t(),
    allowed_tools: [atom()]
  }

  # Optional callbacks based on allowed_tools
  @callback execute(command :: String.t(), context :: map()) ::
    {:ok, String.t()} | {:error, term()}

  @callback read(path :: String.t(), context :: map(), opts :: keyword()) ::
    {:ok, String.t()} | {:error, term()}

  @callback write(path :: String.t(), content :: String.t(), context :: map()) ::
    {:ok, String.t()} | {:error, term()}

  @callback modify(path :: String.t(), old :: String.t(), new :: String.t(), context :: map()) ::
    {:ok, String.t()} | {:error, term()}
end
```

### Callback Mapping

| Claude Tool | Native Callback | Purpose |
|-------------|-----------------|---------|
| `bash_tool` | `execute/2` | Run commands/logic |
| `view` | `read/3` | Read resources |
| `create_file` | `write/3` | Create resources |
| `str_replace` | `modify/4` | Update resources |

## Step 1: Create a Simple Echo Skill

Create `lib/my_app/skills/echo.ex`:

```elixir
defmodule MyApp.Skills.Echo do
  @moduledoc """
  A simple echo skill implemented in pure Elixir.
  """

  @behaviour Conjure.NativeSkill

  @impl true
  def __skill_info__ do
    %{
      name: "echo",
      description: "Echo messages back. Use this to test the native skill system.",
      allowed_tools: [:execute]
    }
  end

  @impl true
  def execute(message, _context) do
    timestamp = DateTime.utc_now() |> DateTime.to_iso8601()
    {:ok, "[#{timestamp}] Echo: #{message}"}
  end
end
```

## Step 2: Use the Echo Skill

```elixir
# Create a session with native skills
session = Conjure.Session.new_native([MyApp.Skills.Echo])

# Define API callback
api_callback = fn messages ->
  body = %{
    model: "claude-sonnet-4-5-20250929",
    max_tokens: 1024,
    messages: messages,
    tools: Conjure.Backend.Native.tool_definitions([MyApp.Skills.Echo])
  }

  Req.post("https://api.anthropic.com/v1/messages",
    json: body,
    headers: [
      {"x-api-key", System.get_env("ANTHROPIC_API_KEY")},
      {"anthropic-version", "2023-06-01"}
    ]
  )
  |> case do
    {:ok, %{status: 200, body: body}} -> {:ok, body}
    {:ok, %{body: body}} -> {:error, body}
    {:error, reason} -> {:error, reason}
  end
end

# Chat
{:ok, response, _session} = Conjure.Session.chat(
  session,
  "Please echo 'Hello from native Elixir!'",
  api_callback
)
```

## Step 3: Create a Log Fetcher Skill

A production skill that fetches logs directly from Elixir:

Create `lib/my_app/skills/log_fetcher.ex`:

```elixir
defmodule MyApp.Skills.LogFetcher do
  @moduledoc """
  Fetch logs from REST APIs using native Elixir HTTP client.
  """

  @behaviour Conjure.NativeSkill

  @impl true
  def __skill_info__ do
    %{
      name: "log-fetcher",
      description: """
      Fetch logs from monitoring APIs. Use this skill when you need to:
      - Retrieve logs from a REST endpoint
      - Filter logs by level or time range
      - Get log statistics
      """,
      allowed_tools: [:execute, :read]
    }
  end

  @impl true
  def execute(command, context) do
    case parse_command(command) do
      {:fetch, endpoint, opts} ->
        fetch_logs(endpoint, opts)

      {:stats, endpoint} ->
        get_stats(endpoint)

      {:error, reason} ->
        {:error, reason}
    end
  end

  @impl true
  def read(path, _context, opts) do
    # "read" can list available endpoints or show help
    case path do
      "endpoints" ->
        {:ok, "Available endpoints:\n- /api/logs\n- /api/logs/stats\n- /api/logs/:id"}

      "help" ->
        {:ok, help_text()}

      _ ->
        {:error, "Unknown path: #{path}. Try 'endpoints' or 'help'."}
    end
  end

  # Command parsing

  defp parse_command(command) do
    cond do
      String.starts_with?(command, "fetch ") ->
        parse_fetch_command(command)

      String.starts_with?(command, "stats ") ->
        [_, endpoint] = String.split(command, " ", parts: 2)
        {:stats, String.trim(endpoint)}

      true ->
        {:error, "Unknown command. Use 'fetch <url>' or 'stats <url>'."}
    end
  end

  defp parse_fetch_command(command) do
    parts = String.split(command, " ")
    endpoint = Enum.at(parts, 1)

    opts = parts
    |> Enum.drop(2)
    |> Enum.chunk_every(2)
    |> Enum.reduce([], fn
      ["--limit", n], acc -> [{:limit, String.to_integer(n)} | acc]
      ["--level", level], acc -> [{:level, level} | acc]
      _, acc -> acc
    end)

    {:fetch, endpoint, opts}
  end

  # API calls

  defp fetch_logs(endpoint, opts) do
    limit = Keyword.get(opts, :limit, 100)
    level = Keyword.get(opts, :level)

    query = [limit: limit]
    query = if level, do: [{:level, level} | query], else: query

    case Req.get(endpoint, params: query) do
      {:ok, %{status: 200, body: body}} ->
        {:ok, Jason.encode!(body, pretty: true)}

      {:ok, %{status: status, body: body}} ->
        {:error, "API error #{status}: #{inspect(body)}"}

      {:error, reason} ->
        {:error, "Request failed: #{inspect(reason)}"}
    end
  end

  defp get_stats(endpoint) do
    stats_url = endpoint <> "/stats"

    case Req.get(stats_url) do
      {:ok, %{status: 200, body: body}} ->
        {:ok, Jason.encode!(body, pretty: true)}

      {:ok, %{status: status}} ->
        {:error, "Stats API returned #{status}"}

      {:error, reason} ->
        {:error, "Failed to get stats: #{inspect(reason)}"}
    end
  end

  defp help_text do
    """
    Log Fetcher Commands:

    fetch <url> [--limit N] [--level LEVEL]
      Fetch logs from the specified URL.
      --limit N     Maximum logs to fetch (default: 100)
      --level LEVEL Filter by log level (DEBUG, INFO, WARN, ERROR)

    stats <url>
      Get log statistics from the endpoint.

    Examples:
      fetch http://monitoring.example.com/api/logs --limit 50
      fetch http://monitoring.example.com/api/logs --level ERROR
      stats http://monitoring.example.com/api/logs
    """
  end
end
```

## Step 4: Use the Log Fetcher

```elixir
session = Conjure.Session.new_native([MyApp.Skills.LogFetcher])

{:ok, response, _} = Conjure.Session.chat(
  session,
  "Fetch the last 50 error logs from http://monitoring.example.com/api/logs",
  &api_callback/1
)
```

## Step 5: Test Native Skills

Create `test/my_app/skills/echo_test.exs`:

```elixir
defmodule MyApp.Skills.EchoTest do
  use ExUnit.Case

  alias MyApp.Skills.Echo

  describe "__skill_info__/0" do
    test "returns valid skill info" do
      info = Echo.__skill_info__()

      assert info.name == "echo"
      assert is_binary(info.description)
      assert :execute in info.allowed_tools
    end
  end

  describe "execute/2" do
    test "echoes message with timestamp" do
      {:ok, result} = Echo.execute("Hello", %{})

      assert result =~ "Echo: Hello"
      assert result =~ ~r/\d{4}-\d{2}-\d{2}T/  # ISO timestamp
    end

    test "handles empty message" do
      {:ok, result} = Echo.execute("", %{})

      assert result =~ "Echo: "
    end
  end
end
```

Create `test/my_app/skills/log_fetcher_test.exs`:

```elixir
defmodule MyApp.Skills.LogFetcherTest do
  use ExUnit.Case

  alias MyApp.Skills.LogFetcher

  describe "__skill_info__/0" do
    test "declares execute and read tools" do
      info = LogFetcher.__skill_info__()

      assert :execute in info.allowed_tools
      assert :read in info.allowed_tools
    end
  end

  describe "read/3" do
    test "returns help text" do
      {:ok, help} = LogFetcher.read("help", %{}, [])

      assert help =~ "Log Fetcher Commands"
      assert help =~ "fetch"
      assert help =~ "stats"
    end

    test "returns error for unknown path" do
      {:error, message} = LogFetcher.read("unknown", %{}, [])

      assert message =~ "Unknown path"
    end
  end
end
```

Run tests:

```bash
mix test test/my_app/skills/
```

## Step 6: Integrate with Ecto

Native skills can access your database directly:

```elixir
defmodule MyApp.Skills.Database do
  @behaviour Conjure.NativeSkill

  @impl true
  def __skill_info__ do
    %{
      name: "database",
      description: "Query the application database for user and order information.",
      allowed_tools: [:execute, :read]
    }
  end

  @impl true
  def execute(query, _context) do
    case parse_query(query) do
      {:users, :count} ->
        count = MyApp.Repo.aggregate(MyApp.User, :count)
        {:ok, "Total users: #{count}"}

      {:users, :recent, limit} ->
        users = MyApp.Repo.all(
          from u in MyApp.User,
          order_by: [desc: u.inserted_at],
          limit: ^limit,
          select: %{id: u.id, email: u.email, created: u.inserted_at}
        )
        {:ok, Jason.encode!(users, pretty: true)}

      {:orders, :stats} ->
        stats = get_order_stats()
        {:ok, Jason.encode!(stats, pretty: true)}

      _ ->
        {:error, "Unknown query. Try 'count users' or 'recent users 10'."}
    end
  end

  @impl true
  def read(table, _context, opts) do
    case table do
      "users" -> {:ok, describe_table(MyApp.User)}
      "orders" -> {:ok, describe_table(MyApp.Order)}
      _ -> {:error, "Unknown table: #{table}"}
    end
  end

  defp describe_table(schema) do
    fields = schema.__schema__(:fields)
    types = Enum.map(fields, &{&1, schema.__schema__(:type, &1)})

    """
    Table: #{schema.__schema__(:source)}
    Fields:
    #{Enum.map_join(types, "\n", fn {f, t} -> "  - #{f}: #{t}" end)}
    """
  end
end
```

## Multiple Native Skills

Combine multiple native skills in one session:

```elixir
session = Conjure.Session.new_native([
  MyApp.Skills.Echo,
  MyApp.Skills.LogFetcher,
  MyApp.Skills.Database
])

# Claude can use any of these skills
{:ok, response, _} = Conjure.Session.chat(
  session,
  "First check how many users we have, then fetch recent error logs",
  &api_callback/1
)
```

## Tool Generation

Conjure automatically generates Claude tool definitions:

```elixir
tools = Conjure.Backend.Native.tool_definitions([MyApp.Skills.LogFetcher])

# Generates:
# [
#   %{
#     "name" => "log_fetcher_execute",
#     "description" => "Execute a command...",
#     "input_schema" => %{...}
#   },
#   %{
#     "name" => "log_fetcher_read",
#     "description" => "Read a resource...",
#     "input_schema" => %{...}
#   }
# ]
```

## Best Practices

1. **Keep skills focused** - One responsibility per skill
2. **Return structured data** - Use JSON for complex outputs
3. **Handle errors gracefully** - Return `{:error, reason}` with helpful messages
4. **Test thoroughly** - Native skills are easy to test with ExUnit
5. **Document commands** - Provide help text via the `read` callback

## Next Steps

- **[Unified Backends](many_skill_backends_one_agent.md)** - Combine Native, Local, and Anthropic backends
- **[README](../../README.md)** - Full API reference