---
title: "Integrate the Python SDK"
url: "https://docs.caracal.run/guides/sdk-python/"
markdown_url: "https://docs.caracal.run/markdown/guides/sdk-python.md"
description: "Install caracalai-sdk, load a runtime profile, spawn agents with async context managers, delegate authority, and use httpx transport injection."
page_type: "page"
concepts: []
requires: []
---

# Integrate the Python SDK

Canonical URL: https://docs.caracal.run/guides/sdk-python/
Markdown URL: https://docs.caracal.run/markdown/guides/sdk-python.md
Description: Install caracalai-sdk, load a runtime profile, spawn agents with async context managers, delegate authority, and use httpx transport injection.
Page type: page
Concepts: none
Requires: none

---

Use `caracalai-sdk` in Python services and agents that need async session lifecycle, delegation, Gateway routing, or ASGI context propagation.

## Install

```bash
pip install caracalai-sdk
```

## Connect

```python
from caracalai import Caracal

caracal = Caracal()
```

`Caracal()` loads `CARACAL_CONFIG`, a `caracal.toml` in the default config path, or environment variables.

## Spawn and call a resource

```python
import httpx
from caracalai import Caracal

caracal = Caracal()

async with caracal.spawn():
    headers = caracal.headers()
    async with httpx.AsyncClient() as client:
        await client.get("https://api.example.com/tickets", headers=headers)
```

`spawn()` is an async context manager. It binds the agent context inside the block and releases the session when the block exits.

To make agents distinguishable in policy and audit, pass `labels` when you spawn. These become `input.principal.labels`, so several agents under one application stay separable without one application per agent. `labels` are descriptive, for policy and audit, not grants; authority always comes from scopes and delegation. The session lifecycle is handled for you: `spawn()` retires the session when the block exits and records it as a `task`, while `spawn_service()` records a heartbeat-leased `service`.

```python
async with caracal.spawn(labels=["refund-agent"]):
    headers = caracal.headers()
```

See [If many agents share one managed application, can policy and audit still tell them apart?](/reference/faq/#faq-008).

## Long-lived service agents

Daemons and workers that outlive a single request use `spawn_service()` instead of `spawn()`. It returns a handle you own: keep the session alive by calling `heartbeat()` on a timer and retire it with `aclose()`. Each `heartbeat()` **renews the lease** — it extends the deadline the coordinator measures liveness against; it does not merely check status. A service session is reaped by the coordinator only if it stops heartbeating before its lease expires. Unlike a `task`, a `service` is **not** subject to the wall-clock TTL sweeper; its lifetime is governed entirely by the lease, so a faithfully heartbeated service runs until you close it.

```python
svc = await caracal.spawn_service(labels=["billing-worker"])
try:
    while running:
        await svc.heartbeat()
        await do_work(caracal.headers())
        await asyncio.sleep(30)
finally:
    await svc.aclose()
```

Service handles take the same `grant=` as `spawn()`: pass `Grant.narrow(...)` to issue a delegation edge that bounds the service's authority for its whole lifetime, exactly as a narrowed task child. The handle's context carries the edge, and closing the handle retires the session.

```python
svc = await caracal.spawn_service(
    labels=["billing-worker"],
    grant=Grant.narrow(["ledger:read"], resource_id="resource://ledger"),
)
```

When the main coroutine spends long stretches awaiting a provider (a streaming response, a slow tool), renewing the lease from the same loop can starve. Pass `heartbeat_interval` to renew the lease from an independent background task, so the lease stays current even while your code is blocked on a long `await`:

```python
svc = await caracal.spawn_service(labels=["voice-worker"], heartbeat_interval=30)
try:
    async for chunk in stream(caracal.headers()):   # lease renews in the background
        await handle(chunk)
finally:
    await svc.aclose()
```

Auto-heartbeat is opt-in (omit `heartbeat_interval` to renew manually). It renews even while your code awaits, and transient renewal errors are logged and retried on the next tick rather than crashing the worker. A renewal cannot run while the event loop is blocked synchronously (CPU-bound work with no `await`); in that case the lease correctly lapses, which is the liveness signal working as intended.


## Spawn a narrowed child

```python
from caracalai import Grant, DelegationConstraints

async with caracal.spawn() as parent:
    async with caracal.spawn(
        parent_ctx=parent,
        grant=Grant.narrow(
            ["tickets:read"],
            resource_id="https://api.example.com/tickets",
            constraints=DelegationConstraints(max_hops=1, budget=5),
            ttl_seconds=600,
        ),
    ):
        headers = caracal.headers()
```

A plain `caracal.spawn()` runs the child under its parent's effective authority — the application's authority for a root parent, or the parent's narrowed slice when the parent was itself narrowed (transitive least-privilege). Pass `grant=Grant.narrow(...)` only when the child should hold a smaller subset, or `grant=Grant.none()` for a child with no inherited authority.

Use `async with caracal.bind(ctx)` before handing a captured context to a background task.

## Use httpx transport injection

```python
async with caracal.spawn():
    async with caracal.transport() as client:
        await client.get("https://api.example.com/tickets")
```

The transport injects Caracal envelope headers and rewrites configured resource-bound URLs through the Gateway.

## ASGI propagation

After a verifier boundary, use the SDK middleware to bind an inbound Caracal envelope into request context:

```python
from fastapi import FastAPI
from caracalai import Caracal

caracal = Caracal()
app = FastAPI()
app.add_middleware(caracal.context_middleware())
```

The middleware propagates context. It does not verify JWT signatures or revocation; use a connector or transport verifier at the trust boundary.

## Troubleshooting

| Symptom | Check |
| --- | --- |
| `Caracal.from_env: missing ...` | Confirm profile or required environment variables. |
| `headers()` refuses root identity | Bind a context or pass `allow_root=True` only for trusted service-root calls. |
| Background task loses context | Capture the context and rebind with `async with caracal.bind(ctx)`. |
| Gateway routing misses | Confirm resource bindings and `gateway_url`. |

Related pages: [Protect a FastMCP App](/guides/protect-fastmcp/) and [Run an Agent with caracal run](/guides/runtime-run/).
