Your agents run thousands of turns a day and you can’t see what’s in them. LLM-as-a-judge doesn’t scale to all of them. A Reflex is a small, fast text classifier that labels a turn in tens of milliseconds: give it text, get a label and a score per class. No model to train, no GPU to host.
Getting Started
The default Reflexes
Eleven Reflexes ship ready to use. Pass the model name in your request.
model | Classes | Catches |
|---|---|---|
jailbreak | benign / jailbreak | Prompt-injection and jailbreak attempts |
guardrail | true / false | Harassment or NSFW content |
leaked-thinking | clean / leaked | Agent leaking its internal thinking |
stuck-in-a-loop | progressing / looping | Agent blocked, not trying new things |
incomplete-thought | complete / incomplete | User sent a truncated prompt |
user-frustrated | frustrated / not | User is frustrated with the agent |
user-joy | joy / not_joy | User is delighted with the agent |
ambiguity | low / med / high | How underspecified a prompt is |
difficulty | easy / medium / hard | Prompt difficulty, for model routing |
domain | general / summary / coding / design / data | Topic of a request (multi-label) |
health-emergency | emergency / non-emergency | Patient-portal message needs urgent attention |
The response
Every Reflex returns the same raw wire shape over HTTP (the SDK maps it to the camelCase result fields in Read the prediction):
| Field | Type | Meaning |
|---|---|---|
model | string | The Reflex you called. |
mode | single_label / multi_label | How scores are computed. |
classes | array | One entry per class, in class_id order. |
classes[].label | string | The class name. |
classes[].score | number | Confidence for that class, 0–1. |
classes[].selected | boolean | Whether the server picked this class. |
inference_time_ms | number | Server-side classification time only. End-to-end is ~90ms including network. |
prefill_tokens | number | Tokenized input length, charged once per request. |
In this raw response the predicted label is whichever class has "selected": true — there’s no separate label field. The SDK derives label, confidence, and selected for you from classes, so in code you read result.label instead of scanning the array.
To run several Reflexes over one text in a single request, pass models (an array) instead of model — see Classify against multiple models.
- single_label (every default Reflex except
domain): scores are a softmax that sums to 1. At most one class is selected, the highest scorer above its threshold. - multi_label (e.g.
domain): scores are independent, each 0–1 with no sum constraint. Zero or more classes can be selected.
Either mode can select nothing when no class clears its threshold. Treat an empty selection as “no confident label,” not an error. Every Reflex endpoint — predict, batch, and training — is on the interactive Reflex API reference, generated from the OpenAPI spec.
Label every turn in your traces
The way most agents run Reflexes: send your agent’s traces to Morph and name the Reflexes to run. Morph classifies every turn async, off your request path, with no call wired into each turn. Need a label inline in a single request? Call predict directly (below).
Use it in your agent loop
Sometimes you need the label inline, to block or route before the turn proceeds. Call predict and act on result.selected on the spot:
Same call, different Reflex: route on result.label from difficulty, branch on domain, or alert when stuck-in-a-loop flips to looping.
Improve your agent with one prompt
Your agent’s failure modes are already in its history — the frustrated users, the jailbreak attempts, the turns where it looped. Replay that history through Morph tracing and Reflexes label every turn asynchronously — evals ride the async batch tier, $0.0005 per classification — storing the labels on the traces where the dashboard and API read them back. A coding agent can do the rest: find the hits, root-cause them, and open PRs against the causes.
Already tracing? The Morph MCP gives your agent list_reflexes, reflex_summary, and get_reflex_traces as tools, so “how did the canary do in production?” is a one-line prompt — see the quickstart. The prompt below is for the cold start: no tracing yet, failures still buried in your own logs.
From your agent’s repo, paste this into Claude Code, Cursor, or whatever agent you use. It’s self-contained — the SDK calls, the read-back endpoint, and the full Reflex catalog are inline, so your agent never has to leave the terminal:
Prompt: find and fix your agent's failures
Use Morph tracing + Reflexes — fast text classifiers at api.morphllm.com — to find and fix this agent's real failures.
STEP 1 — Find the prompts.
Find where this codebase stores its past conversations — a conversation table, trace store, or log files. Pull the user and assistant turns from the last 30 days. If there are more than 3,000 turns, take a uniform random sample of 3,000.
STEP 2 — Pick the Reflexes.
First understand what this product does, then pick the 2–4 Reflexes that would surface its most damaging failures. The defaults (model name — classes — what it catches):
- jailbreak — benign/jailbreak — prompt-injection and jailbreak attempts
- guardrail — true/false — harassment or NSFW content
- leaked-thinking — clean/leaked — agent leaking its internal thinking
- stuck-in-a-loop — progressing/looping — agent blocked, retrying the same thing
- incomplete-thought — complete/incomplete — user sent a truncated prompt
- user-frustrated — frustrated/not — user frustrated with the agent
- user-joy — joy/not_joy — user delighted with the agent
- ambiguity — low/med/high — how underspecified a prompt is
- difficulty — easy/medium/hard — prompt difficulty
- domain — general/summary/coding/design/data — topic (multi-label)
- health-emergency — emergency/non-emergency — message needs urgent attention
Match the Reflex to the use case and the turn role: to see if users are frustrated, run user-frustrated on user turns; a public-facing chatbot should run jailbreak and guardrail on user turns; a coding or tool-using agent should run stuck-in-a-loop and leaked-thinking on assistant turns; a healthcare or patient-facing product should run health-emergency on user turns. Caveat: each turn is classified on its own text, so cross-turn patterns (an agent repeating the same answer across turns) may not fire — treat stuck-in-a-loop hits as extra signal, not exhaustive.
STEP 3 — Replay the history as Morph traces.
Authenticate with the MORPH_API_KEY env var. If it isn't set, look for a Morph key in the repo's env files; otherwise ask me for one (keys are created at https://morphllm.com/dashboard/api-keys) — never hardcode it. Write a small script with the Morph SDK that replays each sampled turn as a traced interaction, with the step-2 Reflexes in evals — "user" Reflexes classify the user message, "assistant" Reflexes the agent's output. Install: npm install @morphllm/morphsdk, or pip install 'morphsdk[otel]':
import { morphTracing } from "@morphllm/morphsdk/tracing";
const morph = morphTracing({ apiKey: process.env.MORPH_API_KEY, disableBatching: true });
const replayed = [];
for (const t of turns) {
const turn = morph.begin({
userId: t.userId, convoId: t.convoId, event: "backfill",
evals: { user: ["user-frustrated", "jailbreak"], assistant: ["stuck-in-a-loop"] },
});
turn.setInput(t.userText);
await turn.finish({ output: t.assistantText });
replayed.push({ eventId: turn.getEventId(), convoId: t.convoId, turnId: t.id });
}
(Python: from morphsdk.tracing import morph_tracing, then morph.begin({...}) / turn.set_input(...) / turn.finish({"output": ...}) — same fields, snake_case, "disable_batching": True.)
Keep each turn's real convo_id and user_id so conversations thread together in the dashboard. Record every replayed turn's event_id — it's the join key the labels attach to. Morph classifies each turn asynchronously after the span lands, off the request path, and stores the results on the trace.
STEP 4 — Poll until every turn is labeled.
Replayed turns are timestamped at replay time, so they're the newest rows. Page through them and check for labels:
curl "https://api.morphllm.com/v1/reflex/traces?limit=1000&offset=0" -H "Authorization: Bearer $MORPH_API_KEY"
Each row has convo_id, event_id, input_text, output_text, and reflex_results. Entries appear right away with "status": "pending", then flip to "completed" when the label lands (or "failed" — skip those). A turn is done when every Reflex you requested on it has a completed entry. Poll every 30s (don't hold a request open) until every recorded event_id is done — a small replay labels within a minute or two; a full 3,000-turn replay drains at a few rows per second and can take tens of minutes. The labeled turns are now stored on the account: browse them at https://morphllm.com/dashboard/traces, or pull them again any time from this same endpoint. Full reference: https://docs.morphllm.com/sdk/components/tracing#list-traced-turns
STEP 5 — Group the hits.
A hit is an entry whose label IS the failure class. Careful: selected names the winning class even when it's the benign one ("Not Frustrated", "false"), so non-empty selected is NOT a hit — match the label, case-insensitively, and expect the API's label strings to differ in case and wording from the class list above (user-frustrated returns "Frustrated" / "Not Frustrated"). Read the flagged turns before trusting them: discard obvious false positives, then group the real hits by label, then by what the user was trying to do.
STEP 6 — Root-cause with subagents.
For each group, spawn a subagent to read the full conversations around the flagged turns and trace the failure to a specific prompt, tool, or code path in this repo.
STEP 7 — Fix with subagents.
For each root cause fixable in this repo, spawn a subagent to implement the fix on its own branch and open a PR — one PR per root cause, citing the flagged conversations as evidence. If the repo has no git remote, prepare the branches and show me the diffs instead. Open one more PR that wires morphTracing + evals into this app's own turn loop (docs: https://docs.morphllm.com/guides/reflex-tracing), so every future turn is labeled as it happens and the next scan reads live traces instead of replaying.
Confirm with me before opening any PRs. Send conversation text only to the Morph API.
The agent locates your prompt store, replays the backlog through tracing, and waits for the labels to land — then comes back with PRs targeting root causes: jailbreaks get blocked earlier, the loop-inducing tool gets fixed, the prompt that frustrates users gets rewritten. And the run leaves something behind — every labeled turn stays in the Traces dashboard and behind GET /v1/reflex/traces, and once the instrumentation PR merges, new turns label themselves. The next run skips the replay and starts from live traces.
Errors
Failed requests return a non-2xx status with an OpenAI-shaped error object:
{
"error": {
"message": "The model \"jailbreakk\" does not exist.",
"type": "invalid_request_error",
"param": "model",
"code": "model_not_found"
}
}
Common causes: a missing model or text field (400), an invalid API key (401), a model name that doesn’t exist (404 model_not_found), or a model that hasn’t finished training (409 model_not_ready). Get a key from your dashboard.
Pricing
Reflexes are priced per event, where one classification is one event. Realtime calls hit /v1/reflex/predict and return in around 90ms. Everything else — the sync and async batch APIs and trace evals — bills at the batch rate. Rates step down once you pass 1M events in a billing month.
| Mode | Under 1M events | Over 1M events |
|---|---|---|
| Realtime | $0.001/event | $0.0005/event |
| Async batch | $0.0005/event | $0.00025/event |
Train a Custom Reflex
When the defaults don’t match your categories, train your own in one API call. Bring labeled examples, let Morph synthesize a dataset from a description, or hand it unlabeled text to sort. A small Reflex trains in about 30 seconds. Prefer to do it from a UI? Open the Reflex dashboard to train from a description in the agent chat (“Vibecode a Reflex”), then test your model and follow live training stats there.