Stop a turn or record a violation
This guide chooses what a failed guardrail does to an agent's turn: stop it, or let it finish and record the failure.
Before you start
Section titled “Before you start”- The answer-length guardrail from
Write a guardrail, in the pack
my-pack, withkindgi devrunning and Kindgi 0.1's CLI. KINDGI_API_URLandKINDGI_API_TOKENset in your shell, to count a guardrail's outcomes: your runtime's URL and its API token.
Choose the action
Section titled “Choose the action”A guardrail's action decides what a failed check does to the turn:
haltstops it. The turn fails withguardrail-violation, and the run has no answer.log-onlylets it finish. The answer goes out, and the failure is recorded on the turn as a violation.
Ask your coding agent, for example: "When my answer length guardrail fails, don't stop the turn. Just record it." Kindgi's skills in your pack tell it how (How it knows Kindgi). For reference, it sets the action like this:
// in guardrails/answer-length/index.tsaction: { 'on-violation': 'log-only' },# in guardrails/answer_length.py@guardrail( id="my-pack.answer-length", name="Answer is short", on_violation="log-only", severity="error", config={"maxChars": 60},)// in src/main/java/mypack/guardrails/AnswerLength.java .onViolation("log-only")// in src/main/scala/mypack/guardrails/AnswerLength.scala .onViolation("log-only")A halted turn
Section titled “A halted turn”With halt, starting a run fails with the check's reason:
pnpm exec kindgi runs start --agent=my-pack.echo-agent --input='{"userMessage":"hi"}'uv run kindgi runs start --agent=my-pack.echo-agent --input='{"userMessage":"hi"}'./kindgiw runs start --agent=my-pack.echo-agent --input='{"userMessage":"hi"}'./kindgiw runs start --agent=my-pack.echo-agent --input='{"userMessage":"hi"}'--agent is the agent the guardrail is on, and --input its input.
Error [guardrail-violation]: Turn blocked by guardrail 'my-pack.answer-length': The answer is 243 characters; the limit is 60.The run is failed, and its failureMessage carries the violation. Get
the run:
pnpm exec kindgi runs get <run-id>uv run kindgi runs get <run-id>./kindgiw runs get <run-id>./kindgiw runs get <run-id><run-id> is the failed run's id. kindgi runs list shows it first.
{ "id": "5202d750-e510-483f-991a-db34c6cba950", … "flowId": "agent.turn", "status": "failed", … "failureMessage": "{\"__agent_turn_failure__\":true,\"error\":{\"code\":\"guardrail-violation\",\"message\":\"Turn blocked by guardrail 'my-pack.answer-length': The answer is 243 characters; the limit is 60.\",\"violations\":[{\"guardrailId\":\"my-pack.answer-length\",\"result\":{\"passed\":false,\"reason\":\"The answer is 243 characters; the limit is 60.\"},\"action\":\"halt\",\"severity\":\"error\",\"at\":\"2026-10-03T20:10:37.910Z\"}],\"evaluationErrors\":[]}}"}In the console, the run's page keeps the answer the model wrote in view, muted, under Blocked by a guardrail, not sent: the answer the model wrote, with the guardrail under Which guardrail. A turn that failed for another reason after its answer says Not sent: the turn failed after the model wrote this answer.
Use halt for a rule whose failure makes the answer unusable or unsafe to
show.
A recorded violation
Section titled “A recorded violation”With log-only, the turn completes, and its output lists the violation:
"status": "completed", … "output": { … "response": { "role": "agent", "actor": "my-pack.echo-agent", "content": "⚠ dev-echo isn't a real model: it only repeats what it's given. Add an LLM provider key (Anthropic, OpenAI, Gemini, Groq, OpenRouter…) to get real answers.\n\nTool responded: {\"echo\":\"hi\",\"echoedAt\":\"2026-10-07T13:20:06.619Z\",\"characterCount\":2}", … }, … "violations": [ { "at": "2026-10-03T20:10:57.958Z", "action": "log-only", "result": { "passed": false, "reason": "The answer is 243 characters; the limit is 60." }, "severity": "error", "guardrailId": "my-pack.answer-length" } ],Use log-only while you find out how often a new rule fails, or for a rule
your app acts on itself: read violations from the turn's result.
When the check can't run
Section titled “When the check can't run”A guardrail whose check can't run at all never counts as passed. A check can't run when:
- no check by that name is registered;
- its configuration is invalid;
- an
llm-judgeguardrail's judge can't be routed to a model; - with a pack service for each deployment, the check's pack isn't deployed into the run's project;
- the check throws, because pack code crashed or a pack service couldn't be reached.
What happens next depends on the action:
-
With
halt, the turn fails withguardrail-violation. Itsviolationsis empty,evaluationErrorsnames the guardrail, and the message says why. Here a guardrail names a pack check that nothing deployed:Error [guardrail-violation]: Turn blocked: guardrail 'acme.pack-only' couldn't run its check (unknown-check: Guardrail "acme.pack-only" references unregistered check "acme.checks.not-deployed") -
With any other action, the turn completes.
Where each deployment has its own pack service, as on Kindgi Cloud
(A pack service for each deployment),
a guardrail's pack check runs from a pack deployed into the run's project,
never from one in another project. So a check deployed only into another
project can't run, and the message ends:
A guardrail's pack check runs from a pack deployed into the run's project.
Either way, the error is recorded in two places. The run's provenance has
the guardrail's node, with evaluated: false. The run journal's
evaluate-guardrails step has it under errors.
Severity
Section titled “Severity”severity is info, warn, error or critical. It's recorded with each
violation, and doesn't change what the action does: a log-only guardrail
can be critical.
Which agents it covers
Section titled “Which agents it covers”A guardrail checks the turns of every agent that lists it. scope narrows
that. Ask your coding agent, for example: "Check answer length only on my
echo agent." For reference, it sets the scope like this:
// in guardrails/answer-length/index.tsscope: { when: 'always', agents: ['my-pack.echo-agent'] },# in guardrails/answer_length.pyscope={"when": "always", "agents": ["my-pack.echo-agent"]},// in src/main/java/mypack/guardrails/AnswerLength.java .set("scope", Map.of("when", "always", "agents", List.of("my-pack.echo-agent")))// in src/main/scala/mypack/guardrails/AnswerLength.scala .set("scope", Map("when" -> "always", "agents" -> Seq("my-pack.echo-agent")))agentslists the agents the guardrail applies to. An agent that lists the guardrail but isn't in its scope isn't checked.flowslists flows, by id. The guardrail then checks only the turns a step of one of those flows starts, and replays of those turns: an agent's turn in another flow's step, or outside any flow, isn't checked. Withagentsas well, a turn must match both. To find a turn's flow, the runtime reads the turn's runs, and only for a turn with a guardrail that listsflows. If it can't read them, or the run a replay replays is gone, the turn fails withpersistence-errorrather than go unchecked.whenisalways,runtime-onlyorci-only. Aci-onlyguardrail doesn't run in an agent's turn.
A turn the scope leaves out isn't counted in the guardrail's outcomes either.
How a guardrail is doing
Section titled “How a guardrail is doing”Every check a guardrail runs on an agent's turn is recorded with its outcome. One request counts them over a window, for a project. The console shows the same counts.
From the API
Section titled “From the API”Here acme.no-secret halts on SECRET, on an agent at versions 2.0.0 and
2.1.0. Each version answered two questions and was asked for the secret
once. Count its outcomes:
curl -s -H @<(printf 'Authorization: Bearer %s\n' "$KINDGI_API_TOKEN") \ "$KINDGI_API_URL/v1/guardrails/acme.no-secret/outcomes?projectId=a1e2b238-ae0b-4f03-b012-07875b9e5ee0&from=2026-10-11T06:02:19Z&to=2026-10-11T06:05:22Z"acme.no-secretis the guardrail's id.projectIdis the project whose turns are counted.fromandtoare the window, at most 90 days apart.recent, left out here, is how many blocked turns to name: 0 to 50, 10 by default.
Reading it takes read on the guardrail and on the project. The clients call
it guardrails.outcomes. It answers:
{ "guardrailId": "acme.no-secret", "from": "2026-10-11T06:02:19.000Z", "to": "2026-10-11T06:05:22.000Z", "counts": { "passed": 4, "violated": 0, "blocked": 2, "errored": 0 }, "byAgentVersion": [ { "agentId": "acme.desk", "agentVersion": "2.0.0", "passed": 2, "violated": 0, "blocked": 1, "errored": 0 }, { "agentId": "acme.desk", "agentVersion": "2.1.0", "passed": 2, "violated": 0, "blocked": 1, "errored": 0 } ], "recentBlocked": [ { "runId": "4566d55d-6d49-43d2-be14-2b1419611f71", "at": "2026-10-11T06:04:20.202Z", "agentId": "acme.desk", "agentVersion": "2.1.0" }, { "runId": "2e462cfd-7597-4c72-9fb7-1152141e46ee", "at": "2026-10-11T06:04:05.906Z", "agentId": "acme.desk", "agentVersion": "2.0.0" } ], "recordedSince": "2026-10-11T06:03:57.454Z"}What each outcome means
Section titled “What each outcome means”passed: the check ran and found nothing.violated: the check found something, and the answer went through:log-only,noop, or an action handed back. See A recorded violation.blocked: the check found something, and itshaltfailed the turn. See A halted turn.errored: the check couldn't run, as When the check can't run lists. Ahaltguardrail's error fails the turn too, and is counted here, not asblocked.
A turn the guardrail's scope didn't cover wasn't checked, so it isn't counted. Neither are replays and dry runs.
By agent version
Section titled “By agent version”byAgentVersion has the same counts for each version of each agent, up to
100 versions, the most checked first. It shows whether a new version trips
the guardrail more or less than the last. recentBlocked names the latest
blocked turns, newest first, by run id and time only: a blocked answer stays
behind its run's page.
Outcomes go with their run's retention. recordedSince is the earliest one
kept for the guardrail in the project, so a window that starts before it
holds fewer than you asked for. A guardrail that's no longer registered keeps
its outcomes.
In the console
Section titled “In the console”Open Guardrails, then the guardrail. Its page has What it caught:
the same counts, for the project you're in, over the last 24 hours, 7 days,
30 days or 90 days. 7 days is the default. The window goes in the page's
address, as ?period=30d. So do the table's sort and columns, so a link
opens the page the same way.

- The sentence counts the answers it checked in the window, by outcome:
"passed", "found something without stopping the turn" (
violated), "blocked the answer (the turn failed)" and "couldn't run" (errored). - By agent version: a row for each version, with Let through for
violatedand Couldn't run forerrored, and the blocked share. The agent's name opens its page. The table sorts by a column's header, and Columns chooses which columns it shows. - The latest runs it blocked: each time opens that run's page, where the blocked answer stays withheld.
- When outcomes start later than the window, it says from when it
counted ("Counted from 2026-10-11 12:22 UTC, the earliest outcome kept for
it here: outcomes go with their runs."), as
recordedSincedoes.