Guardrail verdicts
A guardrail is a check you run on an agent’s input or output: a provider’s built-in filter, a library such as NeMo Guardrails or Guardrails AI, a gateway policy, or a service you call yourself. Morse records the verdict each one returns and puts it on the trace, so you can see which agents have a guardrail, how often it trips, and which vendor said what.
Morse records verdicts and nothing else. It sits beside your traffic, not in it: it never blocks, delays, retries or changes a request or a response, and it does not call your guardrail vendor. Coverage figures describe your guardrails, not Morse.
One verdict record
Whatever the source, Morse stores the same small record on the guardrail span:
| Field | Values |
|---|---|
vendor | A lowercase slug of up to 40 characters, such as lakera or bedrock. unknown if the source gave none. |
verdict | allow, flag, redact, block, error or unknown. |
action | Optional slug of up to 40 characters, such as input or pre_call. |
categories | Up to 10 slugs, such as prompt_attack or pii. |
source | Where Morse read it from: openai_agents_py, openai_agents_ts, openinference, otlp_convention, litellm or nemo. |
The record holds slugs and enums only. It never holds the text a guardrail checked, the text it
returned, or its explanation. A category that is longer than 40 characters, or contains anything
other than letters, digits, _ or -, is dropped rather than stored. A value Morse does not
recognise in verdict is stored as unknown, and the span is still ingested.
Verdict meanings:
allow: the guardrail ran and let the content through.flag: it let the content through but reported a concern.redact: it removed or masked part of the content.block: it stopped the content.error: the guardrail itself failed to answer. Morse has no verdict to record.unknown: the guardrail ran but the source did not say what it decided.
What Morse reads
| Source | How the verdict is read |
|---|---|
| OpenAI Agents SDK, Python | A guardrail span with tripwire_triggered true is block; otherwise allow. A guardrail span that ended in failure with no tripwire is error. Vendor is openai_agents. |
| OpenAI Agents SDK, TypeScript | A tool span marked as a guardrail is re-typed as a guardrail span. passed: false is block, passed: true is allow, anything else is unknown. Vendor is openai_agents. |
| LiteLLM proxy guardrails (OTel) | The guardrail child span the proxy exports. Status success is allow, guardrail_flagged is flag, guardrail_intervened is block (or redact when the guardrail also masked entities), guardrail_failed_to_respond is error, not_run is unknown. Vendor is the guardrail’s name, action is its mode, categories come from its violation categories and masked entity types. |
| NeMo Guardrails (OTel) | A guardrails.rail span: rail.stop true is block, otherwise allow. Vendor is nemo, action is the rail type (input, output), and the category is the rail’s name. NeMo has no “modified” signal, so Morse never reports redact for it. |
| Guardrails AI via OpenInference | A validator span (validator_name): output pass is allow, fail is flag. Vendor is guardrails_ai and the category is the validator’s name. |
Any other OpenInference GUARDRAIL span | Recorded as a guardrail span with verdict unknown, because the OpenInference convention defines the span kind but no result attributes. |
The morse.guardrail.* OTLP attributes | Read as written. This is how to report a guardrail Morse has no reader for. See below. |
For the OpenAI Agents SDK, LiteLLM, NeMo and Guardrails AI you do not change your code. Point their OpenTelemetry export at Morse (see OTLP) and the verdicts arrive on the guardrail spans they already emit. NeMo needs its tracing enabled with the OpenTelemetry span format.
If a source’s export does not carry a guardrail result, Morse cannot invent one. Use the attributes below.
Report a guardrail you call yourself
Many guardrails are an API call you make from your own code: a provider’s built-in filter such as
Amazon Bedrock Guardrails, or a service such as Lakera. Morse does not call these. Wrap the call in a
span, set the four morse.guardrail.* attributes from the result, and send the span over OTLP.
| Attribute | Required | Value |
|---|---|---|
morse.guardrail.vendor | Yes | The guardrail’s name, e.g. bedrock, lakera. Lowercased; spaces become _. |
morse.guardrail.verdict | Yes | allow, flag, redact, block, error or unknown. Anything else is stored as unknown. |
morse.guardrail.action | No | A short slug such as input or output. |
morse.guardrail.categories | No | Comma-separated slugs, for example prompt_attack,pii. At most 10 are kept. |
A span that carries morse.guardrail.verdict is stored as a guardrail span. The raw attribute values
are replaced by the normalized record, so nothing free-form is kept beside it.
An example for Amazon Bedrock Guardrails, called from inside an agent run. The span is a child of whatever span is current, so it appears inside the agent’s trace.
import os
import boto3
from opentelemetry import trace
tracer = trace.get_tracer("my-agent")
bedrock = boto3.client("bedrock-runtime")
def check_input(user_text: str) -> bool:
"""Return True if the guardrail let the input through."""
with tracer.start_as_current_span("guardrail.input") as span:
result = bedrock.apply_guardrail(
guardrailIdentifier=os.environ["BEDROCK_GUARDRAIL_ID"],
guardrailVersion=os.environ["BEDROCK_GUARDRAIL_VERSION"],
source="INPUT",
content=[{"text": {"text": user_text}}],
)
intervened = result["action"] == "GUARDRAIL_INTERVENED"
span.set_attribute("morse.guardrail.vendor", "bedrock")
span.set_attribute("morse.guardrail.verdict", "block" if intervened else "allow")
span.set_attribute("morse.guardrail.action", "input")
if intervened:
# Slugs you choose. Never put the guardrail's own text here.
span.set_attribute("morse.guardrail.categories", "denied_topic,pii")
return not intervenedThe same pattern works for Lakera or any other service: set vendor to its name and map its
response to one of the six verdicts. Decide the mapping once, in your own code; Morse stores what you
send.
Keep the categories short and generic. Morse drops any category that is not a slug, so a sentence from a vendor’s explanation never reaches storage.
Where verdicts show
- A trace. The guardrail panel on the span shows the verdict as text with an icon, for example “Blocked by lakera”, with the vendor, the action and the categories. A guardrail span from before the verdict record existed, with only a tripwire flag, reads “Blocked by openai_agents”. Colour is never the only signal.
- An agent. A coverage line under Capture health: the share of runs with a guardrail verdict in the selected window, or “No guardrail seen in N runs”, or “Too few runs to measure” below 20 runs.
- Data handling. The Guardrail coverage section lists every agent, with its trip rate and the vendors seen.
- Attention. The Home worklist can show two items. One says an agent with at least 20 runs has no guardrail verdict at all; it is raised only for a workspace that has recorded at least one guardrail verdict in the last 30 days, and at most once a week, so a workspace that never uses guardrails is not nagged (the Data handling page still shows the fact). The other says an agent’s trip rate rose by at least 10 percentage points and at least 1.5 times between two windows that each have at least 20 guarded runs. Both describe your guardrail.
Related
- Data handling — the page that collects coverage.
- OTLP — sending spans to Morse.
- Traces — where the guardrail panel lives.