Business

When a Tool Must Reject a Caller: Writing the Denial Response

Antonio Evans9 min read

A dark server room rendered in isometric cutaway.
Key points

Key points

  1. 1A denial payload must answer three questions in a fixed schema: whether the caller can try again, under what conditions, and who can unblock the action.
  2. 2Machine-readable reason codes plus a retryable boolean let the calling agent choose between escalation, retry and a clean stop without guessing from prose.
  3. 3Hard denials and soft denials belong in separate lanes, because retrying a policy block wastes tokens while retrying a quota or window block succeeds.
  4. 4Logging the denial decision with the originating request id turns the denial path into audit evidence you can review and test on a schedule.

Why a denial is an interface, not an exception

Most teams treat a permission denial as a failure branch: catch it, log it, return a generic error. That works when a human reads the error. It breaks when an autonomous agent reads it, because the agent's next move depends on facts only your service knows. An agent that reads "you lack permission" cannot tell whether the right response is to request a broader scope, wait out a rate window, or abandon the task entirely. The most recent OWASP risk analysis still ranks sensitive data exposure among the top concerns for teams deploying generative AI, and over-permissive responses by agents are a recurring cause of it per OWASP's LLM risk list as summarized by Lasso Security. A denial is the moment the guardrail actually fires, so it deserves a designed contract.

The practical framing comes from authorization literature: access control for agents answers three questions, who the agent is acting as, which resources it can reach, and which operations it may perform across systems as described by GreenNode. A denial response is the enforcement point where one of those answers came back negative. If the response does not say which question failed, the caller cannot correct it.

There is a second reason to build this carefully. Agent action selection is model-driven, and a model will invent a recovery strategy from whatever text you return. Research on tool call patterns shows the orchestrator decides which specialist or tool to invoke based on the descriptions it can read in the agents-as-tools pattern writeup. That means your denial text becomes part of the agent's decision context. Write it to steer the decision, not to explain your architecture.

The fields a denial payload needs

A denial response should be JSON with a stable shape across every tool you expose. The minimum viable set:

FieldTypePurposeRequired
request_idstringEcho of the caller's request id so logs join across servicesYes
deniedbooleanAlways true on this path, so schema-based callers branch cleanlyYes
reason_codeenum stringMachine-readable cause, for example SCOPE_MISSINGYes
retryablebooleanWhether an identical retry may succeed without any changeYes
retry_after_secondsinteger or nullSet only when retryable is true because of a temporary conditionConditional
causeobjectThe subject, resource and action that failed the checkYes
alternativesarray of stringsCalls the caller may make right now without a new grantYes
escalationobject or nullOwner, channel, ticket reference and expected turnaroundConditional
human_messagestringOne short sentence for display to an end userYes

Two of these carry more weight than the rest. The cause object must name the principal, the resource and the verb separately, because an agent debugging a denial needs to know whether it was the agent identity, the delegated user context or the target record that failed. The alternatives array is what separates a good denial from a dead end. If a caller asked to export a full customer table and the agent only holds a field-restricted scope, list summarize_customer_segment and read_customer_record as permitted calls. That mirrors the field-level and record-level constraint approach recommended in permission playbooks, where agents get whitelisted objects, fields and verbs, with higher-risk actions requiring a separate approval as laid out by Pedowitz Group.

Echoing request_id is not optional. Structured logs with correlation ids are the basis for auditing which agent did what across a tool chain per Scalekit's guidance on tool calling authentication. Without the echo, the denial sits in your logs as an orphan and the caller's trace shows only a failed step.

Here is a working shape:

{
  "request_id": "req_9f2c1a",
  "denied": true,
  "reason_code": "SCOPE_MISSING",
  "retryable": false,
  "retry_after_seconds": null,
  "cause": {
    "principal": "agent:support-triage",
    "delegated_user": "user:4471",
    "resource": "crm.accounts",
    "action": "export_bulk"
  },
  "alternatives": ["crm.accounts.read_one", "crm.accounts.summarize_segment"],
  "escalation": {
    "owner": "[email protected]",
    "channel": "ticket",
    "ticket_ref": "AC-2291",
    "expected_response_hours": 8
  },
  "human_message": "Bulk account export needs a manager-approved scope."
}

Hard denials versus soft denials

The single most useful decision you make is the retryable flag, and getting it wrong costs real money. Conflating retryable and non-retryable failures is described as one of the fastest ways to break a production agent, because blind retries of malformed or unauthorized calls burn tokens for identical failures as n8n's error handling guide explains. A denial should be classified before it leaves your service.

Hard denials are deterministic. The same call with the same credentials will fail forever. These carry retryable: false and always include an escalation target. Examples:

  • SCOPE_MISSING: the agent's token does not carry the required scope.
  • POLICY_BLOCK: a policy rule forbids the action outright, such as exporting regulated fields to an external destination.
  • TENANT_MISMATCH: the resource belongs to a different tenant than the delegated user.
  • FORBIDDEN_TOOL: the tool is not on the agent's allowlist.

Soft denials are temporary. The call is legitimate and the credential is sufficient, but a limit or window blocks it right now. These carry retryable: true and a retry_after_seconds value. Examples:

  • QUOTA_EXCEEDED: a per-agent call budget is spent for the current period.
  • RATE_WINDOW: the downstream service is throttling.
  • APPROVAL_PENDING: a human approval exists but has not been granted yet.

The decision rule is simple: if a retry with the identical payload and identical credentials could plausibly succeed once the stated condition clears, the denial is soft. If not, it is hard. Never set retryable: true without retry_after_seconds, because a model told "you may retry" with no bound will loop.

One subtlety worth building in: distinguish "retry identical" from "retry after changing the request." An input validation rejection is neither soft nor hard in the usual sense. The model must inspect the error and repair the call. Handle those in your orchestration layer with a repair path rather than a retry budget, as the retry-classify pattern recommends by routing validation errors to a repair strategy instead of backoff in the seven-patterns production guide.

SignalHard denialSoft denial
Same call can succeed laterNoYes
retryablefalsetrue
retry_after_secondsnullinteger
Escalation targetAlways presentOnly if approval path applies
Caller actionStop, report, request grantWait, then retry
Example codePOLICY_BLOCKQUOTA_EXCEEDED

Three worked examples

Example 1: scope missing on a bulk export. A triage agent asks for a bulk export of accounts. It holds one-token-per-resource style delegated credential scoped to reads. The service returns SCOPE_MISSING, retryable: false, cause.action: export_bulk, and an escalation block pointing at the RevOps access owner with a ticket reference. It also lists two alternatives that succeed today. The agent's plan changes: it summarizes the segment instead. No human is paged, and the trace shows the denied attempt next to the successful alternative. This is the pattern of task-scoped, ephemeral credentials that expire with the session described by Sweet Security.

Example 2: quota exceeded mid-workflow. A reporting agent has been calling one tool in a loop and hits the per-session budget. The service returns QUOTA_EXCEEDED, retryable: true, retry_after_seconds: 900. Because the response names the condition, the caller's orchestration layer can pause and resume instead of failing the whole run. A circuit breaker that opens after repeated failures and reports a bounded cool-down to the agent is a close cousin of this pattern as shown in the production patterns guide.

Example 3: approval pending on a write. A coding agent proposes a change to a deployment configuration. The policy allows the write only with human approval, so the service returns APPROVAL_PENDING, retryable: true, retry_after_seconds: 300, and an escalation block naming the on-call approver. The general rule for agents is that they may propose high-consequence actions but execution pauses until a human approves per Sweet Security's privilege abuse guidance. The agent stores the fact that it is waiting, reports to the user, and rechecks. Without retry_after_seconds, the agent would either give up too early or hammer your approval queue.

Note what all three share. The caller never has to parse a stack trace or a sentence in English to decide what to do next. The payload short-circuits the model's improvisation.

Logging and audit: turning denials into evidence

Denial logs are your highest-value audit data because they record intent that was blocked. Log these fields immutably for every denial: request id, agent identity, delegated user, tool name, the cause object, the reason code, the verdict, the enforcement action taken, and a timestamp. That set matches the logging expectations for AI agent policy decisions, where records should capture the requested action, agent identity, relevant context, policy evaluated, verdict and outcome as Sweet Security lists.

Three rules make the logs useful rather than merely present.

First, make denial records write-once. The Lasso analysis of runtime enforcement notes that compliance frameworks increasingly require evidence that safeguards are actively enforced during operation, not just documented at design time per Lasso's enforcement overview. A mutable denial log is weak evidence.

Second, log the alternatives you offered along with whether the caller took one. A denial where the caller immediately found a permitted path means your allowlist is well designed. A denial with no follow-up call means the agent gave up or the payload was unclear. That ratio is the metric to watch.

Third, watch for chains that combine low-risk permissions into a high-risk outcome. Read-only in one system plus write in another can still be dangerous when an agent joins the two, which is why access decisions need to be evaluated across workflows and not just within single tools per Pluto Security. Failed denials near a successful chain are the strongest signal that a policy is too coarse.

If you are running agents through a single inference endpoint, that endpoint is the natural place to attach the same request id you emit in denials, so one trace spans model calls and tool calls. Quanta Inference exposes text, image, video, audio and embedding models through one OpenAI-compatible API, which keeps that correlation simple across model types Quanta Inference.

Testing the denial path on purpose

The denial path is the code most likely to rot, because it fires only on the unhappy route. Treat it like any other contract and test it three ways.

Unit contract tests. For every tool, assert that a denial returns all required fields, that denied is true, that retryable: false always carries a non-null escalation, and that retryable: true always carries a positive retry_after_seconds. This is a schema assertion, not a behavior test, and it catches the common drift where a new reason code ships without its mapping.

Agent behavior tests. Give a test agent a denied tool and check what it does next. The expected behaviors are: stops and reports on a hard denial, waits the stated interval and retries on a soft denial with a bounded budget, and picks an alternative when one is listed. This is where most real defects surface, because the model interprets your payload. One practitioner warns that asking the model to self-regulate on policy decisions is not enough; enforcement should be deterministic and not rely on the model as Lasso argues. Your tests are where you verify that the deterministic layer actually bounded the unpredictable one.

Adversarial tests. Craft calls that attempt to trigger the denial indirectly: a prompt injection that steers the agent toward a forbidden tool, a delegated token reused for a tool outside its task, or two permitted reads that together expose another tenant's data. Fine-grained access control that blocks any tool call outside the agent's explicit scope is the stated mitigation for prompt injection risk as Auth0's access control review explains. Test that the block surfaces as a clean POLICY_BLOCK denial, not a 500.

Close the loop by replaying real denials from your logs against a staging build after any policy change. If a previously clean SCOPE_MISSING now returns POLICY_BLOCK, you changed a rule and should know before a customer does.

Who owns the escalation target

The escalation object only helps if the owner it names is real and reachable. Approval workflows for elevated access should route through a ticketed process with business justification and an expiration, rather than an ad hoc request per the access and permissions playbook. Map each reason code to an owner in configuration, not in code, so the mapping is reviewable:

escalation_map:
  SCOPE_MISSING:
    owner: [email protected]
    channel: ticket
    expected_response_hours: 8
  POLICY_BLOCK:
    owner: [email protected]
    channel: ticket
    expected_response_hours: 24
  APPROVAL_PENDING:
    owner: [email protected]
    channel: pager
    expected_response_hours: 1

Review that map on the same cadence as your access recertification. Quarterly reviews that remove dormant accounts and collapse duplicate roles should also validate that no denial points at a person or queue that no longer exists per Pedowitz's governance guidance. A stale owner is worse than none, because it teaches the caller to escalate into a void.

When the denial discipline needs to extend across a whole line of business, an expert that already reads your context and hands back finished deliverables can carry the review load: Quanta Experts are built for exactly that division of work Quanta Experts.

FAQ

FAQ

What is the minimum a denial response must contain?

At minimum: the echoed request id, a boolean denied flag, a machine-readable reason code, a retryable boolean, the cause object naming principal, resource and action, and a list of permitted alternatives. If the denial is hard, add an escalation target with a named owner. If it is soft, add a retry_after_seconds value so the caller knows when to try again.

How do I decide whether a denial is retryable?

Ask whether an identical payload with identical credentials could succeed after a stated condition clears. Quotas, rate windows and pending approvals are retryable because only time or a human is missing. Missing scopes, policy blocks and tenant mismatches are not retryable, because nothing about waiting changes the outcome. Never mark a denial retryable without giving a bounded interval.

Should the denial message be readable by the end user?

Keep two strings: a short human_message for display and the structured fields for the agent. The human message should be one sentence and should not leak policy internals, while the structured cause object gives the agent exactly what it needs. Users see the first, automated callers branch on the second.

How do I test that an agent handles denials correctly without paying for constant real failures?

Inject denials at the tool boundary in a staging environment and assert the agent's next action: stop, wait or pick an alternative. Run schema contract tests on every tool so no denial ships with missing fields. Then replay denials captured in production logs against newly deployed policy changes to catch regressions before users do.

What should denial logs capture to survive an audit?

Log the request id, agent identity, delegated user, tool name, cause object, reason code, verdict, enforcement action and timestamp, and make the record immutable. Also log which alternatives you offered and whether the caller used one, since that ratio tells you whether the policy is too coarse. Regulators increasingly expect runtime evidence that safeguards fired, not just design documentation.

Sources

Sources

  1. AI Policy Enforcement: Protect Data, Models & Systems
  2. Access Control for AI Agents: Managing Identity and ...
  3. Build Multi-Agent Systems Using the Agents as Tools Pattern
  4. How do you ensure agents have correct data access and ...
  5. Tool calling authentication for AI agents
  6. LLM Tool Calling Error Handling: Retries and Fallbacks
  7. 7 Patterns That Stop Your AI Agent From Going Rogue in ...
  8. Privilege Abuse in AI Agents: Risks and Prevention
  9. AI Agent Policy Enforcement: A Complete 2026 Guide
  10. What Is AI Agent Access Control? ...
  11. Access Control in the Era of AI Agents
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles