# How to Fall Back to Default Logic When LLM Output is Unsatisfactory

> Source: <https://pub.towardsai.net/how-to-fall-back-to-default-logic-when-llm-output-is-unsatisfactory-e5cdb3b63964?source=rss----98111c9905da---4>
> Published: 2026-09-25 05:29:02+00:00

A support team wires up a small workflow. Every incoming email goes to an LLM, which returns:

```
{ "category": "billing", "priority": "high", "needs_human": false }
```

The rest of the workflow reads those fields and routes the ticket. It runs fine all week. Then one morning it doesn’t: a request comes back as a friendly paragraph instead of JSON, another times out, and a third returns valid JSON with "priority": "urgent-ish" which parses cleanly but breaks the router. Tickets stop moving, and nobody finds out until a customer escalates.

Everyone will tell you to add a fallback. But this leaves two questions: when should the fallback kick in, and what should it do instead?

It is not that complex; you already have a fallback. Right now it is “crash, or pass something broken downstream.” The job is not to add one. It is to replace a bad one with a better one. This guide works through both questions using the ticket triage workflow above.

Before you can pick a fallback, your system needs a way to tell good output from bad. Bad output shows up in three ways:

The API timed out, hit a rate limit, or returned a 500. This one is easy, because your code already knows. The call raised an error. Most people think of this case first. It is the least of your problems.

The model returned a paragraph instead of JSON. Or valid JSON with a field missing. Or "urgent-ish" in a field where you allow three values.

Schema validation catches all of these. Define the shape you expect, then check every response against it:

``` python
from pydantic import BaseModel, ConfigDictfrom typing import Literal
class Triage(BaseModel):    model_config = ConfigDict(strict=True, extra="forbid")    category: Literal["billing", "technical", "account", "general"]    priority: Literal["low", "medium", "high"]    needs_human: bool
triage = Triage.model_validate_json(raw_output)
```

Without the Literal types, "urgent-ish" sails through and breaks something three steps later, far from where the problem started. With them, you get a clean error the moment it arrives.

The model refused to answer, or it read an angry chargeback threat and marked it low priority. Nothing is malformed, so validation passes, and the workflow carries on with a bad answer.

This is the expensive one, because catching it needs rules from your own business. Here is one you can actually check: if the email mentions a chargeback or a lawyer and the model returned low priority, treat that as a failure.

Every level needs its own check. A timeout surfaces through your error handler, a bad schema through your validator, and a wrong priority through your own business rules. Nothing on that list is caught automatically: *you cannot fall back on what you cannot detect.*

Here are your options, from cheapest to most expensive.

Now, the question is: how do you choose between them? Here is my rule of thumb: a fallback only helps if it fails differently from what it replaces.

Let’s run the options above through it:

``` python
def triage_fallback(email_text):    text = email_text.lower()
if any(word in text for word in ["refund", "invoice", "charge"]):        category = "billing"    elif any(word in text for word in ["login", "error", "crash"]):        category = "technical"    else:        category = "general"
return {        "category": category,        "priority": "medium",        "needs_human": True,    }
```

Those keyword lists are deliberately basic, and a real one would be tuned against your own tickets. Even so, this is worse than the model on a good day and much better than the model on a bad one, because it returns something the workflow can use. The ticket reaches a human queue instead of vanishing.

Ticket triage has an obvious fallback because the output is a small set of choices. Classification, extraction, and routing all work this way. Open-ended writing does not. If the model fails to draft a good reply to a customer, no keyword rule will draft one for you. Your options stop at retrying, escalating, or honestly saying you couldn’t do it.

One caveat, though: retrying gets risky once the first attempt already did something. If it created a ticket before failing, a second attempt creates a second ticket. Retry the model call, not the actions it already took.

In code, you build most of this yourself. In n8n, you configure most of it. One thing to get right first: the model here never picks a tool, so this is a [Basic LLM Chain](https://docs.n8n.io/integrations/builtin/cluster-nodes/root-nodes/n8n-nodes-langchain.chainllm) node, not an [AI Agent](https://docs.n8n.io/integrations/builtin/cluster-nodes/root-nodes/n8n-nodes-langchain.agent) node, which requires a tool.

In your code In n8n try/except around the call [On Error, Continue (using error output)](https://docs.n8n.io/build/understand-workflows/workflow-components/work-with-nodes) Retrying a failed call [Retry on Fail](https://docs.n8n.io/build/understand-workflows/workflow-components/work-with-nodes) The Pydantic model [Structured Output Parser](https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.outputparserstructured/) Retry with the error fed back [Auto-fixing Output Parser](https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.outputparserautofixing/) Backup provider Second chain on the error branch Your business rules [Switch](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.switch/) or [Code](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.code/) node

Open the Basic LLM Chain node’s settings and turn on Retry on Fail. n8n allows up to 5 tries with up to 5 seconds between them, and that wait does not grow. That helps with a temporary failure, but not a long rate-limit window.

For those cases, [n8n’s rate-limit guidance](https://docs.n8n.io/integrations/builtin/handle-rate-limits) recommends controlling the pace instead: use a [Loop Over Items](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.splitinbatches/) node to batch requests, and a [Wait](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.wait/) node to space them out.

For a provider outage, set On Error to Continue (using error output) and route that error output into a second Basic LLM Chain node, with a different provider’s chat model attached. n8n has a [template](https://n8n.io/workflows/5160-build-resilient-ai-workflows-with-automatic-gpt-and-gemini-failover-chain/) that demonstrates this kind of failover.

Turn on Require Specific Output Format and connect a Structured Output Parser. Define the schema you expect.

For our example, that means restricting priority to low, medium, or high, not just saying it is a string. If you generate the schema from a JSON example, n8n reads the field names and types but ignores the example values, so use a JSON Schema with enum values when the values matter.

To let n8n attempt a repair first, wrap the Structured Output Parser in an Auto-fixing Output Parser. When parsing fails, it sends the bad output and the parsing error to another LLM and asks it to fix them.

Apply our test here: the auto-fixer is another model call, so it can fail in some of the same ways. If the repair still fails, send the error output to your plain-code fallback.

A valid response can still be a bad one. n8n calls the step successful as long as the output parses, even if your business rules disagree with the answer.

Add a Switch or Code node after the chain to catch those cases. If the email mentions a chargeback or lawyer but the model says priority = low, send it down the same fallback branch.

If those rules live in a Code node, handle its errors too. It can fail like any other node, and an unhandled error there stops the workflow it is meant to protect.

A fallback nobody looks at is a slow leak. You can inspect executions to see which nodes and branches ran. To measure the rate reliably, record it yourself: [custom execution data](https://docs.n8n.io/build/understand-workflows/understand-executions/customize-executions-data), available on Pro, Enterprise, and registered Community plans, stores string pairs like fallback_used = “true” that you can filter executions by.

For failures that escape every branch, set an error workflow starting with the [Error Trigger](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.errortrigger/) node to alert your team.

A fallback rate that suddenly climbs means something changed upstream. You want to hear that from your workflow, not from a customer.

[How to Fall Back to Default Logic When LLM Output is Unsatisfactory](https://pub.towardsai.net/how-to-fall-back-to-default-logic-when-llm-output-is-unsatisfactory-e5cdb3b63964) was originally published in [Towards AI](https://pub.towardsai.net) on Medium, where people are continuing the conversation by highlighting and responding to this story.
