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

# Run Your Own Brain

> Put your own process at the text boundary of a live call - an AgentRunner serving inbound and outbound phone calls.

The [Quickstart](/get-started/quickstart) placed a call with a `Prompt`
brain and deployed nothing. This page is the `Runner` path: your own
long-lived Python process at the text boundary - for DB reads mid-call, your
own framework, or logic a prompt cannot carry.

The deal is unchanged: your
[`AgentRunner`](/get-started/core-concepts#agentrunner) receives a
transcribed turn as a string and returns a string. No frames, no codecs, no
SIP - the speech pipeline and the carrier stay on Unpod's side.

## Prerequisites

* Python 3.12+ and `pip install unpod`
* `UNPOD_API_KEY` (`sk_...`) in the environment - the runner's bearer, from
  [unpod.ai/api-keys](https://unpod.ai/api-keys/)
* **A number in your account.** Provision one from the dashboard under
  **Dev Platform → Numbers**. No carrier account, no SIP trunk - see
  [Numbers](/speech-stack/numbers).

<Note>
  Already own a number elsewhere? You can bring it over a
  [trunk](/speech-stack/numbers#trunks) instead. That is the only path that needs
  carrier credentials.
</Note>

## Step 1 - Provision the number

Provisioning uses the **Management API** - the REST half of the [SDK](https://github.com/unpod-ai/unpod-python-sdk), reached
through `AsyncClient`. For REST auth it accepts either the org-scoped
`UNPOD_PLATFORM_TOKEN` (with `UNPOD_ORG_HANDLE`) or `UNPOD_API_KEY`; this
page uses `UNPOD_API_KEY`, since the runner needs it anyway. The REST
endpoint derives from `UNPOD_BASE_URL` (default `api.unpod.ai` →
`https://<host>/platform`).

Provisioning is the same `agents.voice.create()` call as the
[Quickstart](/get-started/quickstart) - the only difference is
`brain=Runner()`: your process answers the turns instead of a platform-run
prompt.

If you need one-off overrides in code, pass `base_url=` to `AsyncClient` or
`AgentRunner`; those arguments win over `.env` for that process only.

Run this once to pick a voice profile, create the agent, and attach a free
number from your account:

```python theme={null}
# setup.py - run once to provision your phone number
import asyncio
from unpod import AsyncClient, Runner

async def setup() -> None:
    async with AsyncClient() as client:
        # 1. Pick a voice profile from the read-only catalog.
        profiles = await client.voice_profiles.list(language="en")
        if not profiles:
            print("No voice profiles found.")
            return
        vp = profiles[0]
        print(f"Using voice profile: {vp.name} ({vp.profile_id})")

        # 2. Create the agent. agent_id MUST match your AgentRunner.
        voice = await client.agents.voice.create(
            "my-runner-agent",            # agent_id - must match AgentRunner's
            brain=Runner(),               # your own worker answers turns
            name="my-runner",
            voice_profile=vp.name,        # name (case-insensitive) or profile_id
            recording=True,
            max_call_duration_s=600,
        )
        print(f"Created agent: {voice.agent_id}")

        # 3. Find a number in your account that is not attached yet.
        numbers = await client.numbers.list(status="active")
        free = [n for n in numbers if n.pipe_id is None]
        if not free:
            print("No free numbers - provision one under Dev Platform -> Numbers.")
            return

        # 4. Attach it to the agent.
        number = await client.numbers.attach(free[0].number_id, "my-runner-agent")
        print(f"Attached {number.number} -> agent my-runner-agent")

asyncio.run(setup())
```

What it does:

1. **`voice_profiles.list()`** - pick a voice from the read-only catalog.
2. **`agents.voice.create()`** - create the agent, its brain, and its first voice.
3. **`numbers.list()`** - find a number in your account with nothing attached.
4. **`numbers.attach()`** - route that number to the `agent_id`. It takes
   `agent_id`, not a pipe id.

If step 3 finds nothing, provision a number from the dashboard first.

<Accordion title="Bringing your own number instead">
  BYON numbers arrive over a trunk you register once, then sync into your account:

  ```python theme={null}
  summary = await client.numbers.sync()   # {"synced": int, "new": int}
  ```

  Full setup: [Trunks](/speech-stack/numbers#trunks).
</Accordion>

<Note>
  The agent's `agent_id` (`"my-runner-agent"`) is the same string your
  `AgentRunner` registers under in Step 2. They must match exactly, or
  inbound calls never reach your runner. See
  [IDs You'll Meet](/get-started/core-concepts#ids-youll-meet).
</Note>

## Step 2 - Answer an inbound call

One `entrypoint`, one `AgentRunner`. The brain here is SuperDialog's
`LLMAgent`; any adapter works - Anthropic, OpenAI, LangChain, an HTTP
endpoint, or a playbook (see
[Bring your agent](/speech-stack/bring-your-agent)) - and nothing about the
call path changes:

```python theme={null}
# agent.py - your process at the text boundary
import os
from unpod import AgentRunner, CallContext
from superdialog import LLMAgent

async def entrypoint(ctx: CallContext) -> None:
    ctx.session.dialog_machine = LLMAgent(
        llm="anthropic/claude-haiku-4-5-20251001",
        system_prompt="You are a helpful voice assistant. Keep answers under 3 sentences.",
    )
    await ctx.session.run()

def build_runner() -> AgentRunner:
    return AgentRunner(
        entrypoint=entrypoint,
        agent_id=os.getenv("AGENT_ID", "my-runner-agent"),
        # base_url and api_key derive from UNPOD_BASE_URL / UNPOD_API_KEY
        # unless you pass explicit args here.
    )

if __name__ == "__main__":
    build_runner().start()   # blocking
```

Start the runner:

```bash theme={null}
python agent.py
```

The runner connects to the orchestrator and waits. Now call the number you
attached. Unpod recognises the number, resolves the `agent_id` it is attached
to, and dispatches the call to your waiting runner. Your agent answers
and speaks - your own process at the text boundary, live on a phone call.

<Note>
  The runner does not need to restart when you provision the number. Run `setup.py`
  once, then leave the runner up; it serves every inbound call until you stop it.
</Note>

## If the runner will not connect

Three close codes cover almost every failure:

* **4001** on the control socket - the orchestrator refused the credential.
  Do not retry: a reconnect loop on a bad key is indistinguishable from a
  network outage.
* **4003** on the bridge - the pairing failed: an unknown `call_id`, an
  expired pairing (it expires \~20s after dispatch), or a bad call token.
* **4009** - that call already has a live bridge socket.

## Step 3 - Make an outbound call (optional)

Inbound is one direction. To have your agent place a call, use `calls.create`
with the `agent_id` and the destination number:

```python theme={null}
# call_out.py - dispatch an outbound call
import asyncio
from unpod import AsyncClient

AGENT_ID = "my-runner-agent"   # from setup.py
TO_NUMBER = "+19995550001"

async def make_call() -> None:
    async with AsyncClient() as client:
        call = await client.calls.create(
            agent_id=AGENT_ID,
            to_number=TO_NUMBER,
        )
        print(f"Outbound call {call.call_id} -> {TO_NUMBER} (status: {call.status})")

asyncio.run(make_call())
```

`calls.create` enqueues the call and returns immediately with `status="pending"`.
Unpod dials out, then dispatches the answered call to the same running
`AgentRunner` - your `entrypoint` handles outbound exactly as it handles inbound.

<Note>
  `calls.create` also still accepts `pipe_id=` for older code. `agent_id` wins if
  you pass both, and `to_number` is always required. Outbound dispatch may also
  need `from_number=` depending on your numbers - see
  [Outbound calls](/speech-stack/outbound-calls).
</Note>

## Next steps

<CardGroup cols={3}>
  <Card title="Production setup" icon="list-checks" href="/speech-stack/setup-checklist">
    The full path: trunks, numbers, recording, and deployment.
  </Card>

  <Card title="Outbound calls" icon="phone-outgoing" href="/speech-stack/outbound-calls">
    Campaigns, dynamic instructions, and per-call data.
  </Card>

  <Card title="Use your own agent" icon="bot" href="/speech-stack/bring-your-agent">
    Plug in LangChain, an HTTP endpoint, or any brain you already have.
  </Card>
</CardGroup>
