Back to Blog
When Your Workflow Needs to Be Code, But You Still Want a UI: Exploring n8n
9 min readSep 3, 20264 views

When Your Workflow Needs to Be Code, But You Still Want a UI: Exploring n8n

We've all been there: a repetitive task needs automating, a new system needs to integrate with an old one, or a scheduled job needs to pull data from somewhere and push it elsewhere. You could write a script, but then you're managing cron jobs, error handling, and secret rotation. You could use an e

Full-StackAutomationToolingSoftware DesignAPI
Share

by Sunil Band

The Script vs. SaaS Dilemma

You've probably hit a wall with those repetitive tasks that just need to be automated. Maybe it's syncing data between a CRM and an analytics platform, sending custom notifications based on webhook events, or orchestrating a complex sequence of API calls. Your first instinct might be to write a custom script. Python, Node.js, whatever. It's flexible, it's cheap, and you have full control. But then you're responsible for deployment, monitoring, retries, credentials, and making sure it runs reliably.

On the other hand, there are plenty of SaaS workflow automation tools. Zapier, Make (formerly Integromat), and the like. They offer fantastic UIs, drag-and-drop builders, and abstract away most of the operational headaches. The catch? They can get expensive fast, especially as your usage scales or your workflows become more complex. And the moment you need to do something truly custom, you often hit a hard wall, forced to awkwardly shoehorn a 'code block' into a visual flow, or even worse, abandon the platform entirely.

This is where n8n (pronounced 'n-eight-n') steps in. It aims to bridge that gap, giving you the power of a custom script with the visual comfort and operational benefits of a hosted platform. It's open-source, self-hostable, and provides a rich UI for building workflows, but it doesn't shy away from letting you drop into code when you need to.

Visualizing the Flow, Empowering the Code

What makes n8n compelling is its dual nature. You design your workflows visually, connecting nodes that represent different operations: HTTP requests, database queries, logic gates, data transformations, and integrations with hundreds of services. This visual representation is invaluable for understanding complex flows at a glance and collaborating with non-developers.

But the magic happens when those pre-built nodes aren't enough. Instead of being limited, n8n lets you inject custom JavaScript code directly into your workflow at any point. This isn't just for trivial transformations; you can write substantial logic, interact with external libraries, and perform complex data manipulations. This flexibility is what transforms n8n from 'another automation tool' into a powerful full-stack automation platform.

Let's consider a practical example. Imagine you have a form submission that comes in via a webhook. You want to enrich this data by calling an external API, then conditionally send an email or update a database, perhaps with some custom validation in between. Here's how you might approach this with n8n, using a custom code node for validation and data shaping.

A Real-World Workflow: Data Enrichment and Conditional Logic

Let's set up a workflow that receives form data, validates an email address using a hypothetical external API, enriches the data, and then decides whether to send a Slack message or update a database based on the validation result.

First, we'd start with a Webhook node to receive the form submission. This gives us an endpoint to post data to.

json
[
  {
    "nodes": [
      {
        "parameters": {
          "path": "/form-submission"
        },
        "name": "Webhook",
        "type": "n8n-nodes-base.webhook",
        "typeVersion": 1,
        "position": [240, 240]
      }
    ],
    "connections": {}
  }
]

Next, we'll add a Code node. This is where we'll implement our custom validation logic and prepare the data for the next steps. Let's say we want to check if the email field is present and perform some basic sanitization.

javascript
// Inside the 'Code' node in n8n
const items = [];

for (const item of $input.json) {
  const email = item.email;
  const name = item.name;

  if (!email || !name) {
    // Optionally, you could output to an error branch or log this
    console.error('Missing required fields: email or name', item);
    continue; // Skip this item if validation fails
  }

  // Basic email format check (you'd typically use a more robust regex or library)
  const isValidEmailFormat = /^[^
@]+@[^\n@]+\.[^\n@]+$/.test(email);

  // For demonstration, let's assume an external API call here for more robust validation
  // In a real scenario, you'd make an HTTP request from another node *before* this code block
  // and access its result here.
  const externalValidationResult = { isRisky: false, score: 0.1 }; // Mock result

  items.push({
    json: {
      originalData: item,
      validatedEmail: email.toLowerCase().trim(),
      isValidFormat: isValidEmailFormat,
      isRiskyEmail: externalValidationResult.isRisky,
      userName: name.trim()
    }
  });
}

return items;

This Code node takes the incoming item (from the webhook), performs some checks, and outputs a new JSON object with validated and enriched data. The return items; is crucial; it passes the processed data to the next node in the workflow. Notice how we're accessing $input.json for incoming data and building up an array for $output.json.

Now, let's assume after this Code node, we have an HTTP Request node calling an external email validation service. The result of that node would then feed into another Code node or a Switch node for conditional logic.

json
[
  {
    "nodes": [
      {
        "parameters": {
          "path": "/form-submission"
        },
        "name": "Webhook",
        "type": "n8n-nodes-base.webhook",
        "typeVersion": 1,
        "position": [240, 240]
      },
      {
        "parameters": {
          "functionCode": "// Inside the 'Code' node in n8n\nconst items = [];\n\nfor (const item of $input.json) {\n  const email = item.email;\n  const name = item.name;\n\n  if (!email || !name) {\n    // Optionally, you could output to an error branch or log this\n    console.error('Missing required fields: email or name', item);\n    continue; // Skip this item if validation fails\n  }\n\n  // Basic email format check (you'd typically use a more robust regex or library)\n  const isValidEmailFormat = /^[^
@]+@[^\n@]+\.[^\n@]+$/.test(email);\n\n  // For demonstration, let's assume an external API call here for more robust validation\n  // In a real scenario, you'd make an HTTP request from another node *before* this code block\n  // and access its result here.\n  const externalValidationResult = { isRisky: false, score: 0.1 }; // Mock result\n\n  items.push({\n    json: {\n      originalData: item,\n      validatedEmail: email.toLowerCase().trim(),\n      isValidFormat: isValidEmailFormat,\n      isRiskyEmail: externalValidationResult.isRisky,\n      userName: name.trim()\n    }\n  });\n}\n\nreturn items;"
        },
        "name": "Custom Validation & Data Prep",
        "type": "n8n-nodes-base.function",
        "typeVersion": 1,
        "position": [480, 240]
      },
      {
        "parameters": {
          "url": "=https://api.emailvalidation.com/v1/validate?email={{ $json.validatedEmail }}",
          "sendOnlySet": false,
          "options": {
            "json": true
          },
          "authentication": "predefinedCredential",
          "credentialId": "emailValidationApi"
        },
        "name": "External Email Validation API",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 3,
        "position": [720, 240]
      },
      {
        "parameters": {
          "conditions": [
            {
              "value1": "={{ $node['External Email Validation API'].json['data']['status'] }}",
              "operator": "notEquals",
              "value2": "invalid"
            }
          ]
        },
        "name": "Is Email Valid?",
        "type": "n8n-nodes-base.switch",
        "typeVersion": 1,
        "position": [960, 240]
      },
      {
        "parameters": {
          "channel": "#new-leads",
          "text": "=New valid lead from {{ $node['Custom Validation & Data Prep'].json['userName'] }} ({{ $node['Custom Validation & Data Prep'].json['validatedEmail'] }})",
          "webhookUrl": "{{ $connections.slackOAuth2Api[0].webhookUrl }}"
        },
        "name": "Send Slack Message (Valid)",
        "type": "n8n-nodes-base.slack",
        "typeVersion": 2,
        "position": [1200, 160]
      },
      {
        "parameters": {
          "authentication": "predefinedCredential",
          "credentialId": "myDatabaseCredential",
          "operation": "insert",
          "tableName": "leads",
          "data": "={{ $node['Custom Validation & Data Prep'].json }}"
        },
        "name": "Insert into Database (Valid)",
        "type": "n8n-nodes-base.database",
        "typeVersion": 1,
        "position": [1200, 320]
      },
      {
        "parameters": {
          "channel": "#invalid-leads",
          "text": "=Invalid lead attempted from {{ $node['Custom Validation & Data Prep'].json['userName'] }} ({{ $node['Custom Validation & Data Prep'].json['validatedEmail'] }})",
          "webhookUrl": "{{ $connections.slackOAuth2Api[0].webhookUrl }}"
        },
        "name": "Send Slack Message (Invalid)",
        "type": "n8n-nodes-base.slack",
        "typeVersion": 2,
        "position": [1200, 480]
      }
    ],
    "connections": {
      "Webhook": [
        [
          "Custom Validation & Data Prep",
          0
        ]
      ],
      "Custom Validation & Data Prep": [
        [
          "External Email Validation API",
          0
        ]
      ],
      "External Email Validation API": [
        [
          "Is Email Valid?",
          0
        ]
      ],
      "Is Email Valid?": [
        [
          "Send Slack Message (Valid)",
          0
        ],
        [
          "Insert into Database (Valid)",
          0
        ],
        [
          "Send Slack Message (Invalid)",
          1
        ]
      ]
    }
  }
]

This is a simplified representation, but it demonstrates the core idea: use visual nodes for common tasks, and drop into JavaScript with the Code node for anything custom or complex. n8n's expression language ({{ $node['Node Name'].json['key'] }}) makes it easy to pass data between nodes, even into custom code blocks.

The Code Node: Your JavaScript Sandbox

The Code node in n8n is more powerful than it might seem. It's not just for simple data transformations. You can:

  • Access environment variables: process.env.MY_SECRET for secure credentials.
  • Import npm packages: If you're self-hosting, you can extend n8n's capabilities with custom npm packages, though this requires a bit more setup than just npm install inside the UI.
  • Perform complex logic: Loops, conditionals, array manipulations, cryptographic operations, anything JavaScript can do.
  • Handle errors gracefully: You can use try...catch blocks and output error messages, even splitting a workflow into success and error branches.

This allows you to implement nuanced business logic that would be cumbersome or impossible with purely visual builders. For example, if you need to calculate a dynamic discount based on multiple factors pulled from different APIs, or if you need to generate a cryptographically signed token, a Code node is your best friend.

Trade-offs and Considerations

While n8n offers a fantastic blend of visual and programmatic workflow building, it's not a silver bullet. Here are a few things to keep in mind:

  • Self-hosting complexity: While feasible, self-hosting n8n (especially at scale) requires some DevOps knowledge. You'll need to manage Docker containers, databases (Postgres or SQLite), and potentially message queues like Redis for high availability and concurrent execution. Their cloud offering removes this, but then you're back to a SaaS model.
  • Debugging custom code: Debugging JavaScript inside a Code node can be trickier than in a local IDE. n8n provides logs and the ability to view node inputs/outputs, but deep step-through debugging is not natively available. You'll rely heavily on console.log.
  • Vendor lock-in (subtle): Although it's open-source, your workflows are defined in n8n's JSON format. Migrating complex workflows to another platform or pure code would still be a significant effort. However, the open-source nature means you can always inspect and even modify the platform itself.
  • Performance at scale: For extremely high-throughput, low-latency scenarios, a dedicated, optimized microservice might still be more performant than an n8n workflow. n8n is excellent for connecting systems and automating business processes, but it's not designed for millions of requests per second.

I've personally found the trade-offs to be worth it for many common automation tasks. The ability to rapidly prototype complex integrations with the visual editor, then drop into a Code node for the bespoke logic, is a huge productivity win. It frees up developers from writing boilerplate API glue code and allows them to focus on the truly custom parts.

Wrapping up

If you're tired of writing one-off scripts that become maintenance nightmares, or if you're hitting the limits of purely visual automation tools, n8n is definitely worth a closer look. It offers a powerful middle ground that respects both developer's need for code and operations' need for visibility.

To get started, spin up a local instance using Docker. It's the quickest way to kick the tires and build your first workflow. Clone their n8n-docker-compose repository and run docker-compose up -d. From there, you can dive into the UI and start experimenting with webhooks and code nodes. Try to automate one small, recurring task you currently do manually, or one API integration you've been putting off. You'll quickly see the potential.

More from the blog
Available for projectsReady to make something fun 🎈

Ready to build the next system?Wanna build something awesome together?

Currently accepting high-impact opportunities in frontend engineering and scalable web applications.Got a cool idea rattling around? Let's grab a virtual coffee and turn it into something people love. ☕