> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://respan.ai/docs/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://respan.ai/docs/_mcp/server.

# Instructor (tracing)

> Trace Instructor structured-output LLM SDK calls with Respan.

[Instructor](https://python.useinstructor.com/) is a Python LLM SDK for extracting structured data from model responses. Respan captures Instructor calls as chat spans and uses `respan-tracing` for workflow names, metadata propagation, and export.

Instructor does not create its own workflow trace object. Use `@workflow`, `@task`, and `propagate_attributes` from `respan-tracing` when you want recognizable trace names around Instructor calls.

#### Set up Respan

Create an account at [platform.respan.ai](https://platform.respan.ai) and grab an [API key](https://platform.respan.ai/platform/gateway/api-keys).

Run `npx @respan/cli setup` to set up with your coding agent.

#### Use Respan Gateway

See [Instructor gateway setup](/docs/gateway/instructor) to route this integration through the Respan gateway.

#### Example projects

* Example repo root: `respan-example-projects/python/tracing/instructor`

## Setup

#### Install packages

```bash
pip install respan-tracing respan-instrumentation-instructor instructor openai
```

#### Set environment variables

```bash
export RESPAN_API_KEY="YOUR_RESPAN_API_KEY"
export OPENAI_API_KEY="YOUR_OPENAI_API_KEY"
```

`RESPAN_API_KEY` exports traces to Respan. `OPENAI_API_KEY` is used by the OpenAI client in this tracing-only example.

#### Initialize and run

```python
from typing import Literal, TypedDict

import instructor
from openai import OpenAI
from respan_tracing import RespanTelemetry, workflow
from respan_tracing.exporters import propagate_attributes
from respan_instrumentation_instructor import InstructorInstrumentor

telemetry = RespanTelemetry(
    app_name="instructor-support-api",
    is_auto_instrument=False,
)
InstructorInstrumentor().activate()

client = instructor.from_openai(
    OpenAI(),
    mode=instructor.Mode.TOOLS,
)

class SupportTicket(TypedDict):
    customer: str
    priority: Literal["low", "medium", "high"]
    summary: str

@workflow(name="support_ticket_extraction")
def extract_ticket(message: str) -> SupportTicket:
    return client.create(
        response_model=SupportTicket,
        max_retries=2,
        messages=[
            {
                "role": "user",
                "content": f"Extract the support ticket from this message: {message}",
            }
        ],
        model="gpt-4o-mini",
    )

with propagate_attributes(
    customer_identifier="customer_123",
    thread_identifier="ticket_thread_abc",
    metadata={"feature": "support-ticket-extraction"},
):
    ticket = extract_ticket(
        "ACME cannot log in before a security audit and needs help today."
    )
    print(dict(ticket))

```

#### View your trace

Open the [Traces page](https://platform.respan.ai/platform/traces) to see the named workflow and the Instructor chat span. High-level Instructor calls are named by API when context is available, such as `instructor.create`, `instructor.create_with_completion`, `instructor.create_iterable`, and `instructor.async_create`.

## Configuration

| Parameter             | Type           | Default | Description                                                                                               |
| --------------------- | -------------- | ------- | --------------------------------------------------------------------------------------------------------- |
| `api_key`             | `str \| None`  | `None`  | Falls back to `RESPAN_API_KEY` env var.                                                                   |
| `base_url`            | `str \| None`  | `None`  | Falls back to `RESPAN_BASE_URL` env var.                                                                  |
| `app_name`            | `str \| None`  | `None`  | Service name attached to exported spans.                                                                  |
| `is_auto_instrument`  | `bool`         | `True`  | Set to `False` when activating `InstructorInstrumentor` manually to avoid duplicate generic OpenAI spans. |
| `customer_identifier` | `str \| None`  | `None`  | Default customer identifier for all spans.                                                                |
| `metadata`            | `dict \| None` | `None`  | Default metadata attached to all spans.                                                                   |
| `environment`         | `str \| None`  | `None`  | Environment tag, for example `"production"`.                                                              |

`InstructorInstrumentor()` does not require constructor options. Activate it after `RespanTelemetry` is initialized.

## What gets captured

| Instructor API                       | Captured by Respan                                                         |
| ------------------------------------ | -------------------------------------------------------------------------- |
| `client.create(...)`                 | One chat span for the structured-output request.                           |
| `client.create_with_completion(...)` | One chat span plus the parsed response returned to your code.              |
| `client.create_iterable(...)`        | One chat span with the consumed iterable output after iteration completes. |
| `client.create_partial(...)`         | One chat span for the partial-object request.                              |
| Async clients                        | The same span behavior for `AsyncInstructor`.                              |

Captured attributes include prompt messages, serialized output, model, token usage, provider, response schema in `llm.request.functions`, `customer_identifier`, `thread_identifier`, and custom metadata.

## Attributes

### In RespanTelemetry

Set defaults at initialization. These apply to Instructor spans exported by this telemetry instance.

```python
from respan_tracing import RespanTelemetry
from respan_instrumentation_instructor import InstructorInstrumentor

telemetry = RespanTelemetry(
    app_name="structured-output-api",
    is_auto_instrument=False,
    customer_identifier="user_123",
    metadata={"service": "extraction-api", "version": "1.0.0"},
)
InstructorInstrumentor().activate()
```

### With propagate_attributes

Override per request using a context scope.

```python
from respan_tracing.exporters import propagate_attributes

with propagate_attributes(
    customer_identifier="user_456",
    thread_identifier="conversation_abc",
    metadata={
        "route": "invoice-extraction",
        "example_script": "01_create.py",
    },
):
    invoice = client.create(
        response_model=Invoice,
        messages=[{"role": "user", "content": "Extract this invoice..."}],
        model="gpt-4o-mini",
    )
```

| Attribute             | Type   | Description                                                        |
| --------------------- | ------ | ------------------------------------------------------------------ |
| `customer_identifier` | `str`  | Identifies the end user in Respan analytics.                       |
| `thread_identifier`   | `str`  | Groups related structured-output calls into a conversation or job. |
| `metadata`            | `dict` | Custom key-value pairs. Merged with default metadata.              |

## Decorators

Decorators are optional for capture, but recommended for Instructor because Instructor itself does not create named workflow traces. Use `@workflow` to name an extraction pipeline and `@task` to split multi-step parsing.

```python
from typing import TypedDict

from respan_tracing import task, workflow

class Invoice(TypedDict):
    vendor: str
    invoice_id: str
    total_usd: float

@task(name="extract_invoice")
def extract_invoice(text: str) -> Invoice:
    return client.create(
        response_model=Invoice,
        messages=[{"role": "user", "content": text}],
        model="gpt-4o-mini",
    )

@workflow(name="invoice_processing")
def invoice_processing(raw_invoice: str) -> None:
    invoice = extract_invoice(raw_invoice)
    print(dict(invoice))

invoice_processing("Northwind invoice NW-1042 totals 298.00 USD.")
```

## Examples

### Structured extraction with `create`

```python
from typing import TypedDict

class InvoiceLine(TypedDict):
    sku: str
    quantity: int
    unit_price_usd: float

class Invoice(TypedDict):
    vendor: str
    invoice_id: str
    line_items: list[InvoiceLine]
    total_usd: float

invoice = client.create(
    response_model=Invoice,
    messages=[
        {
            "role": "user",
            "content": "Northwind invoice NW-1042 has 2 LOG-001 at 49.50 and 1 SEC-010 at 199.00. Total is 298.00.",
        }
    ],
    model="gpt-4o-mini",
)
print(dict(invoice))
```

### Parsed output plus raw completion

```python
from typing import Literal, TypedDict

class ReleaseNote(TypedDict):
    title: str
    category: Literal["feature", "fix", "docs"]
    bullets: list[str]

release_note, completion = client.create_with_completion(
    response_model=ReleaseNote,
    messages=[
        {
            "role": "user",
            "content": "Create a release note for preserving Instructor tool schemas in traces.",
        }
    ],
    model="gpt-4o-mini",
)

print(dict(release_note))
print(completion.id)
```

### Multiple objects with `create_iterable`

```python
from typing import Literal, TypedDict

class ActionItem(TypedDict):
    owner: str
    task: str
    status: Literal["new", "blocked", "done"]

items = list(
    client.create_iterable(
        response_model=ActionItem,
        messages=[
            {
                "role": "user",
                "content": "Maya will send the launch checklist. Noah is blocked on legal approval.",
            }
        ],
        model="gpt-4o-mini",
    )
)
print([dict(item) for item in items])
```

### Async create

```python
import asyncio
from typing import TypedDict

import instructor
from openai import AsyncOpenAI

async_client = instructor.from_openai(
    AsyncOpenAI(),
    mode=instructor.Mode.TOOLS,
)

class ProjectBrief(TypedDict):
    title: str
    owner: str
    milestones: list[str]

@workflow(name="project_brief")
async def create_project_brief() -> ProjectBrief:
    return await async_client.create(
        response_model=ProjectBrief,
        messages=[
            {
                "role": "user",
                "content": "Build a project brief for the Respan Instructor tracing launch.",
            }
        ],
        model="gpt-4o-mini",
    )

asyncio.run(create_project_brief())
```

### Hooks

Instructor hooks run inside the same traced request. Use them for local diagnostics; use Respan metadata for filtering and analytics.

```python
from instructor.core.hooks import HookName

def on_completion_kwargs(**kwargs):
    print({"model": kwargs.get("model"), "tool_count": len(kwargs.get("tools", []))})

client.on(HookName.COMPLETION_KWARGS, on_completion_kwargs)

try:
    ticket = client.create(
        response_model=SupportTicket,
        max_retries=2,
        messages=[{"role": "user", "content": "ACME needs urgent login help."}],
        model="gpt-4o-mini",
    )
finally:
    client.off(HookName.COMPLETION_KWARGS, on_completion_kwargs)
```