Advanced n8n: Branching, Error Handling, and Real API Integrations
Level up your n8n workflows with IF logic, error handling, the Code node, webhook security, and Supabase integration.
Once you’ve got the basics down — triggers, action nodes, testing — there’s a gap between “this works in test mode” and “this runs reliably in production and handles real edge cases.”
This guide closes that gap. These are the patterns I use in every production workflow I’ve built for SendJob: conditional branching, error handling, custom code, and direct API integration. Learn these and you can build automations that actually hold up.
IF and Switch Nodes
Most real automations aren’t linear. Something happens, and what you do next depends on the state of the data. That’s where the IF node comes in.
The IF node splits your workflow into two paths: one for when a condition is true, and one for when it’s false. You configure it by setting up one or more conditions — comparisons between fields and values.
Here’s a concrete example from SendJob. When a job status is updated in the database, a webhook fires and n8n receives the new status. What happens next depends on that status:
- If status equals
enroute→ send an ETA text to the customer - Otherwise → do nothing (or handle a different status in a separate branch)
In the IF node:
- Value 1:
{{ $json.status }} - Operator: equals
- Value 2:
enroute
The true branch connects to the Twilio SMS node. The false branch either terminates or connects to another IF for a different condition.
You can chain IF nodes to handle multiple conditions, but once you have more than two or three, it gets messy. That’s when you switch to the Switch node.
The Switch node is a multi-branch split. Instead of true/false, you define multiple output paths — one per value. For job statuses in SendJob, this is cleaner than chaining IFs:
- Route 1: status =
confirmed→ send booking confirmation SMS - Route 2: status =
enroute→ send ETA SMS - Route 3: status =
complete→ send invoice email - Default: → log an unrecognized status for review
Each route is a separate output connector on the node, and each connects to its own downstream nodes. This keeps the workflow readable even when the logic gets complex.
One thing to know: both IF and Switch work on the item level. If your workflow is processing multiple items at once (say, a list of jobs), each item is evaluated independently and routed to the appropriate branch.
Loop Over Items
n8n processes items natively — if a node outputs an array of 50 records, the next node runs once for each of those 50 records automatically. This is different from most programming contexts where you’d write a for loop.
But sometimes you need more control — processing items in batches, waiting between iterations, or making an API call per item without hitting rate limits. That’s when you use SplitInBatches.
The SplitInBatches node takes an array of items and processes them in chunks of a specified size. Set the batch size to 1 and you’re essentially looping one item at a time. Set it to 10 and you process 10 at a time.
The pattern looks like this: SplitInBatches → do your per-item work → connect the “Loop” output back to the input of the work section, and the “Done” output to whatever comes after all items are processed.
The most common gotcha: adding a SplitInBatches node when you don’t need one. If you’re just doing a database insert for every item in an array, you don’t need a loop — the Supabase node will handle the whole array. Only reach for SplitInBatches when you genuinely need per-item control: rate limiting, sequential API calls where each depends on the previous, or explicit batching logic.
Error Handling
This is the piece most beginners skip, and it’s the one that hurts the most in production.
Without error handling, a failing node stops the workflow and the failure is silent — unless you’re actively watching the execution history. In a business context, that means a job completion trigger fires, the Twilio SMS node errors because of a malformed phone number, the customer never gets notified, and you have no idea. That’s a real problem.
n8n gives you two levels of error handling.
Node-level error handling: the On Error option. On any node, click the three-dot menu and look for “Settings.” You’ll see an option for what to do if this node errors: stop the workflow (default), continue and use the error output, or retry. For critical nodes, set it to “Continue” and connect the error output to a notification — a Slack message, an email, a database log. This way, if a Twilio call fails, you know about it immediately instead of finding out three hours later.
Workflow-level error handling: Error Workflows. This is a separate workflow that n8n triggers automatically when another workflow fails. Go to Settings → Error Workflow in any workflow and point it at your error-handler workflow.
Your error-handler workflow receives the error context — which workflow failed, which node, what the error message was — and you can do whatever you want with it: send a Slack alert, log it to Supabase, send yourself an email with the details.
Here’s the setup I use for every production workflow in SendJob:
- A dedicated error workflow that takes the incoming error data and:
- Inserts a row into a
workflow_errorstable in Supabase (with workflow name, node name, error message, timestamp) - Sends a Slack message to a
#ops-alertschannel
- Inserts a row into a
- Every production workflow points to this error workflow in its settings
Set this up once, point everything at it, and you have a centralized error log. When something breaks at 2am, you see it in Slack and have enough detail to fix it.
The Code Node
The Code node lets you write JavaScript directly inside a workflow. It receives the items from the previous node, you process them however you want, and you return the modified items.
When to use it:
- String manipulation that the Set node can’t do
- Complex data transformations or combining fields from multiple sources
- Custom business logic that doesn’t fit any built-in node
- Working with dates and times in specific formats
Here’s a real example: phone number formatting. When someone submits a form, they enter their phone number however they feel like it — (555) 123-4567, 555.123.4567, 5551234567. Twilio requires E.164 format: +15551234567. The Set node can’t do this. The Code node can.
for (const item of $input.all()) {
let phone = item.json.phone;
// Strip everything that isn't a digit
phone = phone.replace(/\D/g, '');
// If it's 10 digits, assume US number and prepend +1
if (phone.length === 10) {
phone = '+1' + phone;
} else if (phone.length === 11 && phone.startsWith('1')) {
phone = '+' + phone;
}
item.json.phoneFormatted = phone;
}
return $input.all();
This Code node takes the incoming items, formats each phone number, adds a new field phoneFormatted, and passes the items downstream. The next node — the Twilio SMS node — uses {{ $json.phoneFormatted }} as the recipient number.
A few things to know about the Code node:
- You have access to the full JavaScript standard library, including things like
Date,JSON, string methods, and array methods. - You do not have access to Node.js-specific modules (
fs,http, etc.) by default. n8n sandboxes the execution. - The return value must be an array of items in n8n’s format. If you return something else, the workflow errors. The safest pattern is to modify
item.jsonproperties and return$input.all()as shown above. - Errors in the Code node are real JavaScript errors. Use the test workflow button and read the error messages — they’ll tell you exactly what went wrong.
The HTTP Request Node
This is the single most powerful node in n8n. Any service with an API can be called with it. You don’t need a built-in integration.
The key fields:
- Method: GET, POST, PUT, PATCH, DELETE — match what the API documentation says
- URL: the endpoint URL, with path parameters either hardcoded or referenced with expressions
- Authentication: None, API Key, Bearer Token, Basic Auth, OAuth2 — add your credential here
- Body: for POST/PUT/PATCH, the JSON body you’re sending. In n8n this is usually set as “JSON” body type with fields added manually or as a raw JSON expression.
- Headers: any custom headers the API requires
Reading API documentation and translating it to the HTTP Request node is a skill, and it’s learnable. Here’s the mental model:
- Find the endpoint in the docs — e.g.,
POST /v1/payment_links - Check what auth it needs — Stripe uses Bearer token (your secret key)
- Check the request body — what fields are required, what are optional
- Check the response — what fields does it return, and what will you reference downstream
Real example: creating a Stripe payment link via the HTTP Request node.
- Method: POST
- URL:
https://api.stripe.com/v1/payment_links - Auth: Bearer token, credential pointing to your Stripe secret key
- Body type: Form URL Encoded (Stripe’s API uses form encoding, not JSON — check the docs)
- Body fields:
line_items[0][price]= your price ID,line_items[0][quantity]= 1
The response comes back with a url field — that’s your payment link. Reference it downstream with {{ $json.url }} to include it in an email or SMS.
When an API returns a non-2xx status code, the HTTP Request node errors by default. Use the “Continue on Fail” option (in node settings) if you want to handle API errors gracefully rather than stopping the workflow.
Supabase Integration Deep Dive
The n8n Supabase node handles the four core database operations: SELECT, INSERT, UPDATE, DELETE.
Setting up the credential: you need two things from your Supabase project — the Project URL (found in Settings → API) and the Service Role Key (also in Settings → API — use the service role key, not the anon key, for server-side operations). Add these to a new Supabase credential in n8n.
SELECT: Fetches rows from a table. You set the table name and optionally add filters. Filters are field comparisons — status equals open, created_at greater than a timestamp, etc. You can add multiple filters. The node returns all matching rows as separate items.
INSERT: Creates one or more new rows. Map each column name to the value you want to insert. Use expressions to reference data from upstream nodes — e.g., {{ $json.email }} from a webhook payload.
UPDATE: Updates existing rows based on a filter. Same setup as SELECT for the filter, plus the fields you want to change.
DELETE: Removes rows matching a filter. Be careful with this one.
n8n Supabase node vs HTTP Request node: the built-in node covers 80% of use cases cleanly. Use the HTTP Request node when you need to call Supabase’s REST API directly for something the node doesn’t support — like calling a Postgres function (RPC), using complex PostgREST filters, or interacting with Supabase Storage. The REST API URL pattern is https://[project-ref].supabase.co/rest/v1/[table] with your service role key as a Bearer token and apikey header.
One thing that catches people: Row Level Security. If RLS is enabled on a table and you’re using the service role key, RLS is bypassed — you have full access. If you’re using the anon key, RLS policies apply. For n8n automations, use the service role key and manage access at the application level.
Webhook Setup and Security
Webhooks are how most real workflows start. Here’s the full setup.
Creating a webhook trigger: add a Webhook node as the first node in your workflow. Set the HTTP method (usually POST for data submissions). Set the path — this is the unique identifier in your webhook URL. Something like new-job-inquiry or stripe-events. n8n generates two URLs: a test URL and a production URL.
Test URL vs production URL: the test URL only works when you have the workflow editor open and are actively listening. Use it to test while building. The production URL is always active once the workflow is activated. Point your real integrations at the production URL.
Adding webhook security: anyone who knows your webhook URL can hit it. To prevent unauthorized requests, add a shared secret. The pattern:
- Generate a random secret string (use a password generator, 32+ characters)
- Configure your sending system to include this secret as a custom header — e.g.,
X-Webhook-Secret: your-secret-here - In your n8n workflow, add an IF node immediately after the webhook: check that
{{ $request.headers['x-webhook-secret'] }}equals your expected secret - If it doesn’t match, terminate the workflow (don’t process anything)
- Store the secret as an n8n environment variable rather than hardcoding it in the IF node
Working with nested webhook payloads: webhook data is often nested. A Stripe event payload has the actual data nested under data.object. Reference it with {{ $json.data.object.customer_email }} or similar. If you’re unsure of the structure, trigger the webhook with a test payload and click the Webhook node in the execution to see the raw incoming data.
Environment Variables
Hardcoding values like API endpoints, feature flags, or environment identifiers directly in nodes is a bad habit. When you need to change them, you have to find every place they’re used.
n8n supports environment variables for self-hosted instances via the .env file (or however you’re passing environment to your n8n container). For n8n cloud, you can use the built-in credential system as a proxy for config values, or use a simple “config” table in Supabase that you query at the start of workflows.
For self-hosted, add your variables to the n8n environment and reference them in nodes using {{ $env.YOUR_VARIABLE_NAME }}. Common uses:
APP_ENV— set toproductionorstagingso workflows can behave differently in each environmentNOTIFICATION_EMAIL— an ops email address used across multiple error workflowsSUPABASE_URL— so you can point all workflows at a different database by changing one value
Even if you only have one environment right now, building the habit of using environment variables for anything that might change or differ between environments will save you significant pain when your setup grows.
Now that you know the patterns, let’s build something real. Build It: Lead Capture Automation →