Why Your AI Automation Refuses a Normal Task
An AI automation refuses a normal task when a provider safety classifier or the model itself declines the request, and the API still returns HTTP 200, so no error fires. Each provider flags it in a different field: stop_reason refusal on Anthropic, a refusal field or content_filter finish reason on OpenAI, and finishReason SAFETY or promptFeedback.blockReason on Gemini. The model can also refuse in plain text with a normal stop reason, which no field catches. The fix is to check for all three before parsing and route refusals to a fallback model or a human, never back into the same retry loop.
An AI automation refuses a normal task when a provider safety classifier or the model itself declines the request, and the API still answers with HTTP 200, so your error handling never fires. Anthropic marks it with stop_reason: "refusal", OpenAI with a refusal field or finish_reason: "content_filter", and Gemini with finishReason: "SAFETY" or promptFeedback.blockReason. There is also a fourth kind that no field catches: the model writes "I can't help with that" as ordinary text and stops normally. Most automations check none of these, which is how an apology ends up saved in a CRM note field.
What does a refusal look like to your automation?
It looks like success. The HTTP status is 200, the run shows green in n8n or Zapier, and the next step receives something. What it receives depends on the provider and on when the refusal happened.
On Claude, Anthropic's refusal documentation (checked September 2026) says a declined request returns "a normal response, not an error." On the current Fable, Opus, and Sonnet 5.5 models, a classifier refusal carries a stop_details object whose category names the policy area: cyber, bio, frontier_llm, reasoning_extraction, or general_harms, or null when it maps to none. A refusal before any output leaves content empty. A refusal mid-stream leaves whatever text had already streamed, and Anthropic says to treat that partial output as incomplete and throw it away.
That second case is the dangerous one. Half a drafted reply looks like a reply. A step that grabs content[0].text without reading stop_reason will pass it along as finished work.
On OpenAI, a model-level refusal arrives as a separate field rather than as your content. With structured outputs, OpenAI's docs explain the reason: a refusal "does not necessarily follow the schema you have supplied," so it goes in message.refusal on Chat Completions, or as an output content item with type: "refusal" on the Responses API. Your JSON parser gets nothing, or throws, while the explanation sits in a field the node never mapped. Separately, finish_reason: "content_filter" means content was omitted because OpenAI's filters flagged it.
On Gemini, blocked output is simply missing. Google's safety settings page says the blocked content "is not returned." If the output was blocked, the candidate carries finishReason: "SAFETY". If the prompt itself was blocked, there are no candidates at all and promptFeedback.blockReason says why, with values like SAFETY, BLOCKLIST, PROHIBITED_CONTENT, and OTHER. A step that reads candidates[0] will fail with an index error that looks like a bug in your workflow, not a policy decision.
The four refusal shapes, per provider
This is the table we check against when we wire an AI step. The first three rows are flagged refusals, and the fourth is what they cost. The last row is the one that bites later, because nothing flags it.
| Shape | Anthropic (Claude) | OpenAI | Google (Gemini) | What a naive step does |
|---|---|---|---|---|
| Refused before any output | stop_reason: "refusal", empty content, stop_details.category set | message.refusal string (Chat Completions) or type: "refusal" item (Responses) | promptFeedback.blockReason set, no candidates | Saves blank, or crashes on a missing index |
| Refused mid-output | stop_reason: "refusal" with partial text in content | finish_reason: "content_filter", content omitted | finishReason: "SAFETY", blocked content not returned | Saves half an answer as if it were whole |
| Refusal under a JSON schema | Same refusal stop reason, schema output absent | refusal field instead of schema output | Same block fields, no JSON | Parser throws, retry loop starts |
| Billed? | Before output: only bio, frontier_llm, reasoning_extraction (Sept 2026). Mid-stream: input plus streamed output | Not specified in the refusal docs | Not specified in the safety docs | Nobody notices until the invoice |
| Soft refusal in plain text | stop_reason: "end_turn", text says it cannot help | finish_reason: "stop", text says it cannot help | finishReason: "STOP", text says it cannot help | Saves the apology as the answer |
Two things jump out. First, no two providers use the same field, so a check written for one provider silently passes refusals from another when someone swaps the model. Second, the soft refusal row has no provider signal at all. It is also the one that most often slips into business automations, because it looks exactly like success.
Why do legitimate business tasks get refused?
Classifiers read text, not intent, and your automation feeds them text you did not write. Anthropic's docs are candid about this: benign cybersecurity work can trigger the cyber category, beneficial life sciences work can trigger bio, and benign work can trigger general_harms. These are false positives by design, tuned to err toward declining.
In practice the trigger is usually the input, not your prompt. Typical patterns in operator workflows:
- A support-ticket summarizer at an IT services firm gets a customer email that pastes a suspicious script and asks "is this malware?" The summarization task is harmless. The pasted payload is what the classifier reads.
- A clinic's intake extractor processes a free-text form describing a medication dose or a self-harm history. The model is asked to pull fields, not give advice, but the content sits in sensitive territory.
- A collections or legal-notice drafter is asked to write firm language about consequences, and the model softens it or declines with a paragraph of caveats.
- A product-description generator for a retailer that sells hunting gear, pest control chemicals, or supplements writes a disclaimer instead of copy.
The last two usually produce soft refusals: end_turn, polite text, no flag. That is why the flagged fields are necessary but not enough.
How do you catch a soft refusal?
Give the model a legal way to say no. This is the part the vendor docs skip, because it lives in your schema, not their API.
If your output schema only has fields like summary and category, a model that will not do the task has two options: stuff an apology into summary, or produce something vague that technically fits. Both pass validation. Add an explicit exit instead:
{
"status": "completed | cannot_complete",
"reason": "string, required when status is cannot_complete",
"summary": "string",
"category": "string"
}
Then tell the model in the system prompt: if you will not or cannot do this task for any reason, set status to cannot_complete and explain in reason, and do not write an apology into the other fields. With structured outputs enforced (OpenAI strict schemas, Anthropic structured outputs, Gemini responseSchema), the decline arrives as data your workflow can branch on. We covered why schema enforcement beats prompt-only JSON in why AI returns broken JSON; this is the same idea applied to a no.
Keep a phrase match as a backstop, not the main check. Flag outputs where a free-text field starts with "I'm sorry," "I can't," "I cannot," or "As an AI." It catches the model ignoring your exit field, and it misses rewordings, so log every hit and look at them weekly.
What should the automation do after a refusal?
Order matters. Read the refusal signals first, before you parse, save, or count the run as done. The same rule applies to truncation, which we covered in why AI answers get cut off: the stop reason is the first thing your handler reads, not the last.
Then route by type:
- Classifier refusal (flagged field set). Do not retry the same model with the same input. It is a content decision, not a network blip, and on Claude some categories bill each attempt. Anthropic now offers a beta
fallbacksparameter ("default"with theserver-side-fallback-2026-07-01header) that reruns a refused request on a recommended model inside one call. It is not available on Bedrock, Google Cloud, Foundry, or the Batches API, where you use the SDK middleware or your own retry to a different model. - Soft refusal (
cannot_completeor phrase match). Send the record to a human review queue with thereasonattached. A person can usually finish the task in a minute, and the reason tells you whether the prompt needs a change. - Blocked input (Gemini
blockReason, empty content before output). Consider redacting or trimming the triggering input and retrying once, for example summarizing an email body without the pasted attachment text. If it still refuses, go to the human queue.
Whatever the branch, make it visible. Write the provider, model, refusal type, and category to your run log, next to the record ID. Refusals cluster: one customer's recurring email thread, one product line, one form field. You will not see the cluster unless you count them. Our list of what to log in every automation has the rest of that record.
One more rule for retry logic in general. Most retry policies treat any unexpected output as retryable. A refusal is the opposite of a transient error, so exclude it explicitly, or an overnight loop will retry the same refused record until it hits your spending cap. When an automation should retry explains the general split between faults worth retrying and faults that should stop the run.
What to do next
Open the AI step in your busiest workflow and check one thing: what does it do when content is empty or the stop reason is not the normal one? If the answer is "saves whatever came back," add a branch today that checks the provider's refusal field and stops the record from moving forward. Then add the status and reason fields to the schema so soft refusals stop hiding in plain sight.
If your automations call more than one provider, or you are switching models and do not want to rewrite every check, this is the kind of guardrail layer we build into custom AI systems and workflow automation. If you want a second pair of eyes on a workflow that keeps saving odd answers, get in touch.
Frequently Asked Questions
SOURCES & CITATIONS
- Refusals and fallback — Anthropichttps://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback
- Structured model outputs (refusals) — OpenAIhttps://developers.openai.com/api/docs/guides/structured-outputs
- Create chat completion (finish_reason) — OpenAIhttps://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create
- Safety settings — Googlehttps://ai.google.dev/gemini-api/docs/safety-settings
About Alexey Yushkin
Alexey is the founder of GENERAL INFORMATICS LLC. He designs and ships AI and automation systems for businesses and operators across the US.
Related reading
Want this kind of system in your business?
We build practical AI and automation systems for operators. Send us your current workflow and we will show you what to automate first.
