Module 5: Testing in Sandbox Before Production

4. Dry runs and guarding side effects

Description

By the end of this lesson you will be able to run order-triage end to end without triggering its irreversible effects: testing all the logic —receiving the order, classifying it with the agent— without the node that writes to the CRM ever writing anything. You're going to know exactly what a dry run is, why in n8n it isn't a magic button but a design pattern you build yourself, and you're going to handle four ways of protecting yourself: an environment gate with an IF node, diverting the effect to a node that does nothing, deactivating the node by hand, and running up to just before the effect to observe it without triggering it.

This matters because lesson 2's sandbox key is the first line of defense, but it isn't always enough. Sometimes the provider has no sandbox (the modality C you saw), so any write hits production. Sometimes you want to test the logic without depending on the CRM being available. And sometimes, while iterating fast, you don't even want to clutter the sandbox with garbage. For all those cases you need a second line of defense: the ability to run the workflow with the side effect turned off. That's the dry run.

Connection to the module: in lesson 2 you pointed the side effect at a safe destination (the sandbox). This lesson gives you the control to turn it off completely when you need to, which is the checklist's third point. It builds on Module 4's environments: the gate you're going to set up decides what to do based on which environment the workflow is running in —write for real in prod, don't write in dev. And it sets up lesson 5: once you can run the workflow with no effects, pinning its inputs to repeat the identical run becomes natural. The module's underlying idea gets completed here: safe destination (2) + safe inputs (3) + switchable outputs (4).

The fire drill and the letter in the mailbox

Think of a fire drill at a school. The alarm sounds, the kids line up, the teachers count them, everyone goes out through the evacuation routes, someone times how long it took. The complete procedure runs, seriously, as if it were real. And yet, there's no fire, the fire department doesn't actually get called, and ten minutes later everyone's back in the classroom. The drill tests that the procedure works —do people know how to get out? Do the doors open? How long does it take?— without paying the cost of a real fire.

A dry run is your workflow's fire drill. You run the complete procedure —receive the order, classify it, go through all the logic— but without triggering the irreversible part: the write to the CRM doesn't happen, just like the fire doesn't happen. You verify everything else works, without paying the cost of the real effect. "Dry" comes from rehearsing without the real material —a dry run of a maneuver is done without water, without fire, without ammunition— you go through all the motions, but the dangerous element isn't there.

To understand what a dry run protects, you need the other concept: the side effect.

A side effect is an action your workflow takes whose consequence lives outside the workflow and that the workflow can't undo on its own. Think of it as dropping a letter in the mailbox. While the letter's in your hand, you can read it, correct it, tear it up. The moment you drop it in the mailbox, it crossed a line: it isn't yours anymore, it's going to arrive, and there's no way to take it back. That instant —the point of no return— is the side effect. In order-triage, the HTTP Request node that writes to the CRM is dropping the letter in the mailbox: before that node, everything that happened lives inside the run and can be repeated without consequence; the moment the node writes to the CRM, something changed in the outside world and it doesn't undo.

Not everything a workflow does is a side effect. Reading isn't —querying the weather, listing customers, reading an order doesn't change anything outside, even if it uses a bit of quota. Transforming data in memory isn't either —the Code node that cleans an amount, the IF that decides a branch. Side effects are the actions that write, send, charge, or delete outward: writing to the CRM, sending an email, charging a card, publishing a post, deleting a record. They're exactly lesson 2's "never against production" list, and the reason is the same: they have no undo. The dry run is the technique for running the whole workflow except those.

n8n doesn't have a "dry run" button

Here's an important honesty, because it's a common confusion. Some tools have a global "dry run" mode: a switch that says "run everything but don't execute anything that changes the world." n8n doesn't have that. There's no button that, when pressed, magically deactivates every side effect in your workflow. n8n doesn't know which of your nodes are "dangerous"; to it, an HTTP node that reads and one that writes are the same kind of node.

This isn't a flaw in n8n; it's a consequence of "side effect" being a concept from your domain, not the tool's. Only you know that this HTTP node writes to the CRM and that one only reads. So the dry run in n8n is a design pattern: you design the workflow so it can be run dry. You don't turn it on, you build it. And there are four ways to build it, from the most robust to the fastest. Let's look at them.

The hidden side effects

Before the forms, a warning: not every side effect looks as obvious as "an HTTP node that does a POST." Some hide, and they're the ones that bite the most because you don't think to protect them. It's worth training your eye to hunt them in order-triage and in any workflow:

  • The AI Agent node's tools. An agent doesn't just "think": it can have tools connected that act —one tool that queries the CRM, another that creates a record. If the agent decides to use a tool that writes, that's a side effect triggered from inside the agent, harder to see than a loose node on the canvas. When testing the agent, check what tools it has and whether any of them write.
  • The Webhook's response. If order-triage responds something to the system that sent the order, that response also goes out into the world. It's usually harmless, but if the caller makes decisions with it, it counts.
  • A triggered second workflow. An Execute Workflow node calling another workflow inherits all of that other workflow's side effects. A dry run of the first doesn't protect the second's effects unless the second protects them too.

The discipline: before calling a dry run good, go through the workflow node by node and ask yourself at each one "does this change something outside?" The obvious effects you catch on the first pass; the hidden ones —inside an agent, in a response, in a sub-workflow— are the ones that get you if you don't hunt for them on purpose.

Form 1: the environment gate (the main pattern)

This is the form you want for a serious workflow, because it stays inside the workflow and travels with it to every environment. The idea: right before the node that writes to the CRM, you put an IF node that asks "which environment am I in?" and diverts the flow based on the answer.

An IF node is a gate: it evaluates a condition and sends the flow down one of two outputs, true or false, like a guard checking an ID and letting you through door A or door B. You're going to use it to ask about the environment.

For the IF to know which environment it's running in, it reads an environment variable. In Module 4, each instance ended up with its own configuration; suppose you defined a variable called CUMBRE_ENV that's dev, staging, or prod depending on the instance. In an n8n expression, that variable gets read as {{ $env.CUMBRE_ENV }}. The gate looks like this:

                         ┌─ true  (it's prod) ──→ HTTP Request: writes to the real CRM
... → AI Agent → IF ─────┤
   {{ $env.CUMBRE_ENV }} └─ false (not prod) → NoOp: "dry run, writing nothing"
        == "prod"

The IF's condition is {{ $env.CUMBRE_ENV }} equal to "prod". On the prod instance, the condition gives true, the flow goes to the HTTP node and the letter drops into the mailbox: it writes to the real CRM. In dev and staging, the condition gives false, the flow diverts and the HTTP node never runs: it's a dry run. The same workflow, without changing a line, only writes for real in production.

Verify $env access on your instance. Access to environment variables from expressions is controlled by the N8N_BLOCK_ENV_ACCESS_IN_NODE option, which defaults to false —meaning access is allowed by default. But there are reports that on some configurations $env returns empty even though it should work. If your IF isn't reading the variable correctly, check that option in your instance's configuration (the one you set up in Module 4) before assuming the pattern is broken. And if you'd rather not depend on $env, the same gate works by reading any other signal from the environment you already have —for example, a different field in a per-environment credential. The pattern's idea doesn't change; only where "am I prod?" comes from.

And the node that does nothing?

On the false side of the gate I put a NoOp node. It's worth explaining because it's a key piece and sounds like a joke the first time.

NoOp means "No Operation." It's an n8n node whose only job is to do nothing: it receives the data coming in and passes it through unchanged to its output, without touching it, without calling anything, no effect. It looks useless, but it's exactly what you need on the safe side of the gate: a place to send the flow when you want "nothing to happen." It's the equivalent of the drill's evacuation route leading to the yard and not calling the fire department: the flow ends there, calmly, with no consequences.

There's a useful variant of the NoOp for testing: instead of a pure NoOp, put an Edit Fields (Set) node that builds an object saying what the real node would have done. Something like { "dry_run": true, "would_have_written": { "order_id": "ORD-TEST-002", "status": "manual_review" } }. That way, when you run dry, you not only avoid the effect: you leave evidence recorded of what would have happened, which is exactly what lesson 8 is going to want to save as proof the pass ran. The drill doesn't just avoid the fire; it also notes "the firefighters would have come in here."

Worked example: order-triage dry

Let's see it running. You have the gate set up and you're on your dev instance, testing the large-order-manual-review case (the 52,000-peso order that should go to manual review).

You run the workflow. What to expect:

  1. The order comes in, the AI Agent node classifies it as manual_review.
  2. The flow reaches the IF. The condition {{ $env.CUMBRE_ENV }} == "prod" gets evaluated: since you're in dev, CUMBRE_ENV is "dev", the condition gives false.
  3. The flow goes down the false output, to the Edit Fields node, which returns { "dry_run": true, "would_have_written": { "order_id": "ORD-TEST-002", "status": "manual_review" } }.
  4. The HTTP Request node stays gray, unexecuted: on the canvas you see it never got colored in, because the flow never went through it.

You go to the sandbox CRM: no new record. You go to the production CRM: none either. The run was complete —the agent classified, the logic decided— but the letter never dropped into the mailbox. You proved order-triage classifies the large order correctly, without writing a single line to either CRM. That's a dry run done right.

And when you promote to prod and that same order really arrives, the condition is going to give true, the flow is going to go to the HTTP Request, and this time it does write —because in production, writing is the right thing to do. The workflow is the same; the environment decides.

Form 2: deactivating the node by hand (the quick dry run)

For a specific, quick test, without setting up any gate, n8n lets you deactivate a node in the editor. A deactivated node gets skipped when running: the flow passes through it as if it weren't there, passing the data to the next node without running it.

It's done by selecting the node and deactivating it (with the D key over the selected node, or from its menu; check the shortcut for your version). The node shows dimmed on the canvas. You run the workflow, the CRM's HTTP node is deactivated, and the effect doesn't happen.

When to use it: for a thirty-second experiment —"let me see what the agent classifies without it writing anything." When NOT to use it: as your permanent testing strategy. The problem with deactivating by hand is it's fragile and doesn't travel: it's an editor state you have to remember to set and to remove. The day you forget to reactivate it before promoting, you promote a workflow that doesn't write to the CRM in production —a mute order-triage that classifies orders and registers none. Worse, if you deactivate the node, export, and commit, that "deactivated" state can get saved into the JSON and slip through to production. Form 1's gate doesn't have that risk, because it decides on its own based on the environment and doesn't depend on your memory.

Think of it this way: deactivating by hand is like putting your hand in front of the mailbox to not drop the letter today. It works right now, but depends on you remembering; the per-environment gate is a mailbox that only opens in production, with no action needed from you.

Form 3: running up to right before the effect

Sometimes you don't want to run the whole workflow; you want to observe exactly what would reach the CRM node, without running it. That's what n8n's partial execution is for.

n8n lets you run a specific node with "Execute step": you select a node, open its view, and n8n runs that node and the previous nodes needed to give it its input. The key for the dry run is which node you choose: if you run the node before the CRM's HTTP node —say, the node that builds the order's body that would get sent— you see in its output exactly the data that would have gone to the CRM, and the CRM node never runs, because it's after. You observe the finished letter, about to be dropped, without dropping it.

There's a subtlety worth being clear on, because it's a trap: "Execute step" runs the previous nodes. If you run a node that has the side effect before it in the chain, that effect does get triggered to give it its input. In other words: running up to a node protects you from the effects that are after that node, not the ones before it. In order-triage, if the CRM node is the last one, running the second-to-last is safe. But if you had a side effect in the middle of the flow, running a later node would trigger it. Pick your cutoff point thinking about where the effects are, not where your curiosity is.

And there's a second caution, which connects with lesson 6: the previous nodes include the AI Agent node. If that agent runs against a paid model, running up to after it does cost —it isn't a side effect in the sense of "writes outward," but it does burn API budget. That's why lesson 6 is going to teach you to test the agent against a local Ollama model: so that not even that "read-only" part has a cost. A dry run that avoids writing to the CRM but burns ten dollars in LLM calls is only half a dry run.

Form 4: the gate as an explicit "dry run" flag

A variant of Form 1, for when you want to run dry even in production —for example, to verify prod's logic without writing— instead of the gate asking about the environment, have it ask about a dedicated flag, {{ $env.CUMBRE_DRY_RUN }}, which can be "true" or "false" in any environment.

                              ┌─ false (dry_run off) → HTTP Request: writes to the CRM
... → AI Agent → IF ──────────┤
   {{ $env.CUMBRE_DRY_RUN }}  └─ true  (dry_run on)  → Edit Fields: records what it would have done
        == "true"

The difference from Form 1: the environment gate ties the dry run to the place (I never write in dev); the flag ties it to a decision you can turn on and off wherever you want. In practice they get combined: the IF's condition can be "write only if CUMBRE_ENV == prod and CUMBRE_DRY_RUN != true," so that in production you write normally, but you can force a dry run in production by flipping the flag, without touching the workflow. It's the maximum flexibility, and the one you're going to want the day you need to verify something in prod without risking a write.

Test the gate itself

A precaution almost nobody takes, and one that separates real protection from an illusion of protection: you also have to test the gate. A badly written gate —an inverted condition, an $env returning empty that makes the IF always fall down the wrong branch— is worse than no gate at all, because it gives you false peace of mind: you think you're protected and you're not. Before trusting your gate, deliberately run both branches and confirm each does what it should. In dev, run and confirm the flow goes down the dry-run branch and the CRM doesn't get touched. And —the step almost everyone skips— confirm that in prod the condition would give true: you can do this temporarily by forcing the condition's value, or by checking that {{ $env.CUMBRE_ENV }} really is "prod" on that instance. A gate you never tested on both branches is an assumption, not a defense. Treat it like any other workflow logic: don't trust it until you've seen it work.

How the defenses stack: sandbox and dry run together

Now you have two tools for the same danger —writing to the CRM— and it's worth understanding how they relate, because they aren't alternatives, they're layers.

The sandbox key (lesson 2) answers "where do I write?" With it, the side effect happens, but against a mock destination: it writes, but to the test CRM. The dry run (this lesson) answers "do I write?" With it, the side effect doesn't happen: nothing gets written anywhere.

Think of it as two security rings around the mailbox. The inner ring is the sandbox: if you drop the letter, it falls into a test mailbox you can empty. The outer ring is the dry run: the hand never drops the letter. Which one to use depends on what you're testing:

SituationToolWhy
I want to verify the CRM receives the order correctlySandbox (writes to the test one)I need the write to happen to confirm it works; just falling into the mock
I want to verify only the classification logic, with no CRM dependencyDry run (doesn't write)The write doesn't interest me right now; I turn it off to isolate what I'm testing
The provider has no sandbox (modality C)Dry run, mandatoryAny write hits production, so the only safe option is not writing
I'm iterating fast and don't even want test garbageDry runI don't even want to clutter the sandbox while experimenting twenty times a minute
Dress rehearsal before promoting, as faithful as possibleSandbox in stagingI want everything to really happen, against the mock, to catch what a dry run hides

The ideal combination for a complete pass, which you're going to build in lesson 8, uses both at different moments: dry run while you iterate the logic in dev (fast, without cluttering anything), and sandbox when you really rehearse in staging (faithful, with the write happening against the mock). The dry run is for "I'm still building the test"; the sandbox is for "this is the serious run." Having both layers lets you choose how much reality you want at each moment, without ever risking production.

Common mistakes

Believing n8n has a global dry run (conceptual). What happens: someone looks for n8n's "dry run button," doesn't find it, and concludes n8n isn't good for testing without effects —or worse, assumes "running in the editor" is already a dry run and tests against the real CRM without knowing it. Why it happens: other tools have that button, and it's reasonable to expect it. How to spot it: if you didn't set up any gate or deactivate any node, and you still believe you're in dry run, you're not; the workflow is triggering its effects. How to fix it: understand that a dry run in n8n gets built, not activated. Set up Form 1's gate on any workflow with side effects. It's a design piece, like a car's brake: it doesn't come by magic, it gets installed.

Deactivating the node by hand and forgetting to reactivate it (practical and dangerous). What happens: someone deactivates the CRM node for a test, tests, and forgets to reactivate it. They later promote —or the deactivated state slips into the exported JSON— and in production order-triage classifies orders but registers none. Nobody notices until a whole day's worth of orders is missing from the CRM. Why it happens: deactivating is an editor state that becomes invisible after a while; it's easy to forget. How to spot it: before promoting, check that no node is deactivated —in the JSON diff (Module 3), a disabled change jumps out if you normalized well. How to fix it: use deactivating only for one-minute experiments, never as a strategy. For repeatable dry runs, the per-environment gate, which doesn't depend on your memory.

Protecting the write but not noticing the agent's cost (conceptual). What happens: someone sets up the gate that avoids writing to the CRM properly, runs their dry run a hundred times feeling great, and at the end of the month gets the language model's bill, because each of those hundred runs called the paid LLM. Why it happens: it's easy to think "dry run = I don't touch the outside world" and forget that calling a paid model also touches the outside world —your wallet. How to spot it: if your dry run includes a call to a paid model, every run costs money, even though it writes nothing. How to fix it: it's lesson 6. Test the agent against a local Ollama model, so the dry run is truly at zero cost, write and LLM included.

Exercises

Exercise 1 — Classify side effect or not. For each workflow action, say whether it's a side effect (something to protect in a dry run) or not, and why: (a) a Code node that converts "3,500.00" to 3500; (b) an HTTP node that does a POST to create a customer in the CRM; (c) an HTTP node that does a GET to read the weather; (d) a node that sends an email to the customer; (e) an IF node that decides a branch.

See solution

(a) Not a side effect — it transforms data in memory; nothing changes outside the workflow. (b) Yes — a POST to create writes to the CRM; it changes something outside and doesn't undo itself. Protect it. (c) No — a GET to read doesn't change anything outside (it uses a bit of quota, but leaves no trace to undo). (d) Yes, and one of the worst — sending an email reaches a person; there's no recall. Always protect it. (e) No — deciding a branch is logic in memory, with no external consequence.

Why it works: the test is lesson 2's question —"can I undo it without anyone finding out?"— combined with "does it change something outside the workflow?" Reading and transforming: nothing outside changes, they're not side effects. Writing (POST) and sending (email): yes. Notice the HTTP method matters: the same node type is or isn't a side effect depending on whether it reads (GET) or writes (POST/PUT/DELETE).

Exercise 2 — Design the gate. order-triage is going to add, after writing to the CRM, a node that sends a WhatsApp message to the customer when their order gets approved. Design the gate (or gates) so that WhatsApp only gets sent in prod, and describe what happens in dev when testing the happy-path-approve case.

See solution

Same as with the CRM, you put an IF before the WhatsApp node that evaluates {{ $env.CUMBRE_ENV }} == "prod". The true output goes to the WhatsApp node; the false output goes to an Edit Fields that records { "dry_run": true, "would_have_sent_whatsapp_to": "Café Aurora" }.

In dev, testing happy-path-approve: the agent approves the order, the CRM flow is already covered by its own gate (it doesn't write, or writes to the sandbox), and reaching the WhatsApp gate, CUMBRE_ENV is "dev", so the condition gives false and the WhatsApp node doesn't run. No customer receives a message. In the output you see the record of what would have been sent, as evidence.

Why it works: every side effect needs its own protection; WhatsApp is a new effect —and one of the ones reaching a real person, so one of the most important to protect. The pattern is the same as the CRM's, applied again. In a workflow with several effects, each one carries its own gate, or a shared gate wraps them all.

Exercise 3 — Find the dry run's gap. A teammate says: "I set up the gate that avoids writing to the CRM in dev, so my dry run is at zero cost." Their workflow is: Webhook → AI Agent (paid cloud model) → IF (environment) → CRM / NoOp. Is their dry run really at zero cost? If not, what's missing?

See solution

It isn't at zero cost. Their gate protects the CRM write well —that part's perfect— but the AI Agent node runs before the gate and calls a paid cloud model. Every dry run, even though it writes nothing to the CRM, spends a call to the paid LLM. If they run a hundred tests, they pay for a hundred calls.

What's missing: testing the agent against a local Ollama model instead of the paid one (lesson 6). With the agent pointing at a local model, the hundred runs don't write to the CRM and don't cost a cent of LLM. Then, the dry run is truly and completely at zero cost.

Why it works: "zero cost" has two fronts —not cluttering data (covered by the gate) and not burning budget (covered by the local model). It's easy to solve the first and think you're done, forgetting the second. In a workflow with an AI agent, the LLM's cost is usually the larger of the two, precisely because testing means running many times.

Summary and next step

In this lesson you saw what a dry run is —your workflow's fire drill: you run the complete procedure without triggering the irreversible part— and what a side effect is —dropping a letter in the mailbox: an action whose consequence lives outside the workflow and doesn't undo, like writing to the CRM, sending an email, or charging a card. You understood that n8n doesn't have a dry run button: it's a design pattern you build, with four forms. The main one is the environment gate —an IF node that reads {{ $env.CUMBRE_ENV }} and diverts the effect to a NoOp or Edit Fields node when it isn't prod— which travels with the workflow and doesn't depend on your memory. The others: deactivating the node by hand (quick but fragile), running up to just before the effect with "Execute step" (to observe without triggering, taking care the effect is after the cutoff point), and the explicit dry-run flag for running dry even in production. And you saw the classic gap: protecting the write but forgetting the paid AI agent also costs on every run.

With this you check off the checklist's third point: side effects are guarded.

Before moving on you should be able to: define "side effect" with an example; explain why n8n doesn't have a global dry run; and describe the environment gate and why it's better than deactivating the node by hand.

You can already run order-triage with a safe destination, safe inputs, and switched-off outputs. There's still one property from lesson 1 you don't have yet: that the test be reproducible. If every run uses slightly different inputs, you can't compare a change's effect. Lesson 5 goes into pinned data and execution replay: how to freeze a test's inputs to repeat it identically, and how to use n8n's debugging engine —"Debug in editor," "Copy to editor"— as a repeatable testing tool, with its limits clearly marked.

Resources