Skip to main content
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 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.

Open the Traces dashboard

Browse traced conversations and run Reflexes over them.

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.
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 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 already labeled, and in the export, 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.
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.
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.
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.

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.
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.
cURL

List traced turns

Browse the turns you’ve ingested — the same data behind the Traces dashboard. This is how you read async eval results back in code. Use morph.traces.list() in the SDK, or GET /v1/reflex/traces directly.
Tracing is async, so morph.traces.list() reads labels back after they land. For a label synchronously, inside your request, call morph.reflex.predict() instead and read the result directly.
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 statuspending 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.

Reflexes overview

Label every traced turn — jailbreaks, loops, frustration, and more.

Classify a backlog

Run Reflexes over past traces with the async batch API.