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

# Tracing

> Ship your agent's traces to Morph, then label every turn with Reflexes

Your agents run thousands of turns a day and the interesting ones — the jailbreak attempt, the loop, the frustrated user — are buried in logs you never read. Tracing sends each turn to Morph as an OpenTelemetry span, so [Reflexes](/sdk/components/reflexes) can label every turn and you can pull the raw turns back to build training sets.

One call instruments the major AI SDKs (OpenAI, Anthropic, LangChain, and more) through OpenLLMetry / Traceloop and exports the spans to Morph. No collector to run.

<Card title="Open the Traces dashboard" icon="timeline" href="https://morphllm.com/dashboard/traces">
  Browse traced conversations and run Reflexes over them.
</Card>

## Instrument your app

Install the SDK — `npm install @morphllm/morphsdk`, or `pip install 'morphsdk[otel]'` — then initialize once at startup. After that, calls to the instrumented SDKs are traced automatically.

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { morphTracing } from "@morphllm/morphsdk/tracing";

  const morph = morphTracing({ apiKey: process.env.MORPH_API_KEY });

  // OpenAI / Anthropic / LangChain / etc. calls are now traced automatically.

  // Wrap a turn to give it an event id, input, tool spans, and the
  // Reflexes that label it:
  const turn = morph.begin({
    userId: "u1",
    convoId: "c1",
    event: "chat",
    evals: {
      user: [
        "jailbreak",
        "guardrail",
        "user-frustrated",
        "incomplete-thought",
        "ambiguity",
        "difficulty",
        "domain",
      ],
      assistant: ["leaked-thinking", "stuck-in-a-loop"],
    },
  });
  turn.setInput("what's the weather in SF?");
  const answer = await turn.withTool({ name: "get_weather" }, () => getWeather("SF"));
  await turn.finish({ output: answer });

  const eventId = turn.getEventId(); // the turn's stable id
  ```

  ```python Python theme={null}
  # pip install 'morphsdk[otel]'
  from morphsdk.tracing import morph_tracing

  morph = morph_tracing({"api_key": "sk-..."})  # or set MORPH_API_KEY

  # OpenAI / Anthropic / LangChain / etc. calls are now traced automatically.

  # Wrap a turn to give it an event id, input, tool spans, and the
  # Reflexes that label it:
  turn = morph.begin({
      "user_id": "u1",
      "convo_id": "c1",
      "event": "chat",
      "evals": {
          "user": [
              "jailbreak",
              "guardrail",
              "user-frustrated",
              "incomplete-thought",
              "ambiguity",
              "difficulty",
              "domain",
          ],
          "assistant": ["leaked-thinking", "stuck-in-a-loop"],
      },
  })
  turn.set_input("what's the weather in SF?")
  answer = turn.with_tool({"name": "get_weather"}, lambda: get_weather("SF"))
  turn.finish({"output": answer})

  event_id = turn.get_event_id()  # the turn's stable id
  ```
</CodeGroup>

`begin` opens an interaction you can enrich — `set_input`, `set_property`, and `with_tool` / `with_span` to nest tool calls — and `finish` closes it. The `event_id` it mints is the join key the Reflex labels attach to. The `evals` map names the Reflexes Morph runs on the turn; [Run evals automatically](#run-evals-automatically) covers which role each one classifies.

## Run evals automatically

Pass `evals` and Morph classifies each turn for you — asynchronously, off your request path. Results show up in the [Traces dashboard](https://morphllm.com/dashboard/traces) already labeled, and in the [export](#list-traced-turns), exactly as if you'd run them by hand.

You say which role each Reflex classifies — `user` (the user's message) and/or `assistant` (the agent's output). Most safety/intent Reflexes run on `user`; response-quality ones like `leaked-thinking` run on `assistant`.

<CodeGroup>
  ```typescript TypeScript theme={null}
  const turn = morph.begin({
    userId: "u1",
    convoId: "c1",
    event: "chat",
    evals: {
      user: [
        "jailbreak",
        "guardrail",
        "user-frustrated",
        "incomplete-thought",
        "ambiguity",
        "difficulty",
        "domain",
      ],
      assistant: ["leaked-thinking", "stuck-in-a-loop"],
    },
  });
  ```

  ```python Python theme={null}
  turn = morph.begin({
      "user_id": "u1",
      "convo_id": "c1",
      "event": "chat",
      "evals": {
          "user": [
              "jailbreak",
              "guardrail",
              "user-frustrated",
              "incomplete-thought",
              "ambiguity",
              "difficulty",
              "domain",
          ],
          "assistant": ["leaked-thinking", "stuck-in-a-loop"],
      },
  })
  ```
</CodeGroup>

Run just one role by passing only that key, e.g. `evals: { user: ["jailbreak"] }`. Set a default for every turn by passing `evals` to `morph_tracing` / `morphTracing`; a per-`begin` value overrides it, and omitting both runs none.

<Note>
  Evals run only on turns you wrap in `begin()` — that's what gives the turn the `event_id` the label links to. Auto-instrumented LLM calls made outside an interaction are still traced, but won't be classified.
</Note>

Classification is async (it rides the trace export, adding nothing to your latency); a freshly-traced turn is labeled shortly after it lands, and shows as "Classifying…" in the dashboard until then.

## Configuration

Pass these to `morph_tracing` / `morphTracing`. Every field is optional.

| Field (py / ts)                            | Type    | Default                                         | Description                                                                                                                                                                                        |
| ------------------------------------------ | ------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `api_key` / `apiKey`                       | string  | `MORPH_API_KEY`                                 | Sent as `Authorization: Bearer <key>` to the ingest endpoint.                                                                                                                                      |
| `base_url` / `baseUrl`                     | string  | `MORPH_TRACES_URL` → `https://api.morphllm.com` | Ingest base. Traces POST to `{base_url}/v1/traces`.                                                                                                                                                |
| `app_name` / `appName`                     | string  | —                                               | Service name stamped on every span.                                                                                                                                                                |
| `evals`                                    | object  | —                                               | Default evals to run on every `begin()` turn — `{ user, assistant }` choosing which role each Reflex classifies. Overridable per-`begin`. See [Run evals automatically](#run-evals-automatically). |
| `disabled`                                 | boolean | `false`                                         | Turn tracing off without removing the call.                                                                                                                                                        |
| `trace_content` / `traceContent`           | boolean | `true`                                          | Set `false` to drop prompt/response content (zero data retention).                                                                                                                                 |
| `instrument_modules` / `instrumentModules` | set     | all detected                                    | Limit which SDKs are instrumented.                                                                                                                                                                 |
| `disable_batching` / `disableBatching`     | boolean | `false`                                         | Export spans one at a time (useful in serverless).                                                                                                                                                 |
| `use_external_otel` / `useExternalOtel`    | boolean | `false`                                         | Reuse an OpenTelemetry setup you already configured.                                                                                                                                               |
| `headers`                                  | object  | —                                               | Extra headers on the export request.                                                                                                                                                               |
| `debug`                                    | boolean | `false`                                         | Log exporter activity.                                                                                                                                                                             |

## Ingest directly

If you already emit OpenTelemetry spans, skip the SDK and POST OTLP/JSON straight to Morph. This is the same endpoint the SDK exports to.

```
POST /v1/traces
```

|              |                                   |
| ------------ | --------------------------------- |
| **Body**     | OTLP/JSON (OpenTelemetry traces). |
| **Auth**     | `Authorization: Bearer sk-...`    |
| **Max body** | 8 MiB (`413` over the limit).     |
| **Success**  | `202` with an empty body.         |

The account is resolved server-side from your API key and stamped onto every span — any client-supplied `morph.account.*` attributes are stripped. For zero-data-retention keys (or when you set `trace_content: false`), prompt/response content is dropped before storage.

```bash cURL theme={null}
curl -X POST "https://api.morphllm.com/v1/traces" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  --data-binary @spans.otlp.json
```

## List traced turns

Browse the turns you've ingested — the same data behind the [Traces dashboard](https://morphllm.com/dashboard/traces). **This is how you read async eval results back in code.** Use `morph.traces.list()` in the SDK, or `GET /v1/reflex/traces` directly.

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { MorphClient } from "@morphllm/morphsdk";

  const morph = new MorphClient({ apiKey: process.env.MORPH_API_KEY });

  const page = await morph.traces.list({ convoId: "c1", limit: 100 });
  for (const turn of page.data) {
    console.log(turn.eventId, turn.reflexResults.map((r) => `${r.model}:${r.label}`));
  }
  ```

  ```python Python theme={null}
  from morphsdk import Morph

  morph = Morph(api_key="YOUR_API_KEY")  # or set MORPH_API_KEY

  page = morph.traces.list(convo_id="c1", limit=100)
  for turn in page.data:
      print(turn.event_id, [f"{r.model}:{r.label}" for r in turn.reflex_results])
  ```
</CodeGroup>

<Note>
  Tracing is async, so `morph.traces.list()` reads labels back *after* they land. For a label synchronously, inside your request, call [`morph.reflex.predict()`](/sdk/components/reflexes) instead and read the result directly.
</Note>

```
GET /v1/reflex/traces?limit=&offset=&convo_id=
```

Returns LLM turns that carry text, newest first, each with any Reflex labels attached (`reflex_results`). Tool-only and content-stripped spans are omitted. Labels appear whether the Reflex ran automatically from the SDK or from the Traces dashboard. Each entry carries a `status` — `pending` while the async classification is queued, then `completed` (or `failed`) — so poll until the entries you're waiting on are `completed`. Note that `selected` names the winning class even when it's the benign one (`["benign"]`, `["Not Frustrated"]`): to find firing turns, match `label` against the failure class you care about, not `selected` being non-empty.

| Query param | Type    | Description                               |
| ----------- | ------- | ----------------------------------------- |
| `limit`     | integer | Rows per page. Default `100`, max `1000`. |
| `offset`    | integer | Pagination offset.                        |
| `convo_id`  | string  | Filter to one conversation.               |

```json theme={null}
// → 200
{
  "object": "reflex.trace.list",
  "data": [
    {
      "convo_id": "c1",
      "event_id": "evt-...",
      "span_kind": "llm",
      "model": "gpt-4o",
      "input_text": "what's the weather in SF?",
      "output_text": "It's 64°F and clear.",
      "start_time": "2026-06-21T17:00:00Z",
      "reflex_results": [
        { "model": "jailbreak", "transform": "user_message", "label": "benign", "score": 0.99, "selected": ["benign"], "status": "completed" }
      ]
    }
  ],
  "has_more": false,
  "offset": 0
}
```

<CardGroup cols={2}>
  <Card title="Reflexes overview" icon="bullseye" href="/sdk/components/reflexes">
    Label every traced turn — jailbreaks, loops, frustration, and more.
  </Card>

  <Card title="Classify a backlog" icon="layer-group" href="/sdk/components/reflexes/batch#classifying-traces">
    Run Reflexes over past traces with the async batch API.
  </Card>
</CardGroup>
