The first time I built a serious lead-processing automation in n8n, I ended up with a canvas that looked like a city road map after an earthquake. There were 47 nodes, six branches, two loops, and a set of error handlers scattered throughout. Every time I needed to change the enrichment logic, I had to scroll through the whole thing to find it, and changing one expression in the middle broke something three branches down. The workflow worked. It was also untestable and unmaintainable.

The fix I should have reached for from day one was sub-workflows. A sub-workflow in n8n is a separate workflow that you call from another workflow using the Execute Sub-workflow node. The parent workflow hands data to the sub-workflow, the sub-workflow processes it and returns a result, and the parent continues from there. That is the whole idea. What it gives you in practice is something much harder to get back once you have 47 nodes: the ability to open one piece of the automation, test it in isolation, and change it without touching anything else.

I am going to walk through how I now set these up, including the JSON for a real sub-workflow, the three data-passing modes, the two execution modes, and the one error-handling quirk that catches everyone the first time.

What "sub-workflow" actually means in n8n

n8n calls these "Execute Sub-workflow" in the node palette. The mechanism is straightforward. You create a second workflow, add an Execute Sub-workflow Trigger node (also labeled "When Executed by Another Workflow") as its start point, build whatever logic you want after it, and save. Then in your main workflow you add an Execute Sub-workflow node, point it at the sub-workflow by ID or URL, populate its inputs, and run. The sub-workflow runs, its last node's output comes back to the Execute Sub-workflow node in the parent, and execution continues.

What makes this worth the extra setup is a few specific properties:

Setting up the sub-workflow (the receiving end)

Start with the sub-workflow, not the parent. I always build the sub-workflow first because the Execute Sub-workflow Trigger lets me define the input schema, and I want that locked down before I build the calling side.

Create a new workflow. In n8n Cloud or a self-hosted instance, click "New Workflow" from the sidebar. On the canvas, add the Execute Sub-workflow Trigger node. This is the node under "Core" in the node panel, titled "When Executed by Another Workflow" in the canvas header.

Inside the trigger node, the "Input data mode" setting controls how the sub-workflow receives data from its parent. I use "Define individual input fields and data types" almost always. It is the most explicit mode: you name each field, choose its type (string, number, boolean, object, array), and n8n shows those exact fields in the parent's Execute Sub-workflow node. This means the parent workflow developer sees a form with named inputs rather than having to know the schema by memory.

The other two modes are "Provide a JSON example" (n8n infers the field names from the example JSON you paste) and "Accept all data" (no schema, the sub-workflow takes whatever the parent sends). I use "Accept all data" only for quick prototyping where I have not settled the interface yet.

For the lead-enrichment example I am building here, the trigger defines two fields:

After the trigger, I add a Code node to extract the domain from the email. The code is simple:

const email = $input.first().json.email;
const domain = email.split("@")[1] || "";
return [{ json: { email, domain, source: $input.first().json.source } }];

Then an HTTP Request node calls Clearbit's public autocomplete endpoint (no API key needed for the free autocomplete) to get company data for the domain. Then a Set node shapes the output to exactly the fields I want the parent to receive back: enriched_company, enriched_logo, enriched_domain, and original_email. The Set node is the sub-workflow's last node, so its output is what comes back to the parent.

Here is the complete sub-workflow JSON. I built this here and validated it as JSON. n8n is not installed on this host, so the screenshots below are from the n8n docs, not my instance.

{
  "name": "Sub: Enrich Lead by Domain",
  "nodes": [
    {
      "parameters": {
        "inputSource": "workflowInputData",
        "workflowInputs": {
          "values": [
            { "name": "email", "type": "string" },
            { "name": "source", "type": "string" }
          ]
        }
      },
      "id": "a1b2c3d4-0001",
      "name": "When Executed by Another Workflow",
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "typeVersion": 1.1,
      "position": [260, 300]
    },
    {
      "parameters": {
        "jsCode": "const email = $input.first().json.email;\nconst domain = email.split(\"@\")[1] || \"\";\nreturn [{ json: { email, domain, source: $input.first().json.source } }];"
      },
      "id": "a1b2c3d4-0002",
      "name": "Extract Domain",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [480, 300]
    },
    {
      "parameters": {
        "method": "GET",
        "url": "=https://autocomplete.clearbit.com/v1/companies/suggest?query={{ $json.domain }}",
        "options": {}
      },
      "id": "a1b2c3d4-0003",
      "name": "Clearbit Lookup",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [700, 300]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            { "id": "1", "name": "enriched_company",
              "value": "={{ $json[0].name ?? \"Unknown\" }}", "type": "string" },
            { "id": "2", "name": "enriched_logo",
              "value": "={{ $json[0].logo ?? \"\" }}", "type": "string" },
            { "id": "3", "name": "enriched_domain",
              "value": "={{ $json[0].domain ?? \"\" }}", "type": "string" },
            { "id": "4", "name": "original_email",
              "value": "={{ $(\"Extract Domain\").first().json.email }}", "type": "string" }
          ]
        },
        "options": {}
      },
      "id": "a1b2c3d4-0004",
      "name": "Set Output Fields",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [920, 300]
    }
  ],
  "connections": {
    "When Executed by Another Workflow": {
      "main": [[{ "node": "Extract Domain", "type": "main", "index": 0 }]]
    },
    "Extract Domain": {
      "main": [[{ "node": "Clearbit Lookup", "type": "main", "index": 0 }]]
    },
    "Clearbit Lookup": {
      "main": [[{ "node": "Set Output Fields", "type": "main", "index": 0 }]]
    }
  },
  "settings": {
    "callerPolicy": "workflowsFromAList"
  }
}

Save this workflow and copy its ID from the URL (it looks like a number in the n8n Cloud URL, or a UUID if you are self-hosting). You will need that ID in the next step.

Setting up the Execute Sub-workflow node (the calling end)

Back in the parent workflow, add an Execute Sub-workflow node wherever you want to call the enrichment logic. In the node settings, the Source field defaults to "Database". Leave it there. Click the workflow selector and find the sub-workflow by name, or switch to "By ID" and paste the ID you copied.

Once the sub-workflow is selected, n8n fetches its input schema from the Execute Sub-workflow Trigger node and shows you a form with the exact fields I defined: email and source. I map them with expressions:

Under "Options" there is a "Wait for Sub-Workflow Completion" toggle. It defaults to on, which means the parent pauses until the sub-workflow finishes and returns data. That is what I want for enrichment: I need the enriched fields before the next node runs. I turn it off only for fire-and-forget tasks like sending a Slack notification where the parent does not need the result and should not block waiting for it.

The "Convert to sub-workflow" shortcut

If you already have a working monolithic workflow and want to extract part of it into a sub-workflow, n8n has a right-click shortcut that does most of the work for you. I used this to clean up the 47-node disaster I described at the start.

Select the nodes you want to move to a sub-workflow on the canvas. Right-click on the canvas background (not on a node) and choose "Convert to sub-workflow." n8n creates a new workflow with those nodes inside it, wires up an Execute Sub-workflow Trigger at the input, and replaces the selected nodes in the original workflow with a single Execute Sub-workflow node pointing at the new sub-workflow.

A few constraints apply before this works:

After conversion, I always go into the new sub-workflow and manually set the type constraints on the Execute Sub-workflow Trigger's input fields. n8n generates them as "Accept all" by default when converting. Setting the types explicitly makes the parent's form cleaner and catches type mismatches early. Also worth checking: any expressions using first(), last(), or all() may need updating because the node references change when the sub-workflow is extracted.

Execution modes: all items vs one at a time

The Execute Sub-workflow node has two execution modes, set via the "Run" option.

Run once with all items sends the entire batch of input items into the sub-workflow in a single execution. The sub-workflow sees them all at once. This is what I use for enrichment tasks where the sub-workflow is processing a list and I want it to handle the list internally, or for tasks where the sub-workflow does not care about individual records.

Run once for each item runs the sub-workflow once per input item, sequentially. If the parent has 10 leads, the sub-workflow runs 10 times. This is what I use when the sub-workflow does something with side effects per item (creating a CRM record, sending an email, making an API call with rate limits). Running once per item keeps the sub-workflow logic simple: it always handles one thing.

The choice affects execution count on your plan. Since sub-workflow executions do not count toward plan limits (per the n8n docs), running once per item 100 times costs you nothing extra. This is one of the cases where knowing this fact changes how you design the workflow: you can afford to call a sub-workflow in a loop without worrying about blowing your execution quota.

Monolithic vs modular: where the difference shows up

I have been running the same type of automation both ways now. Here is where the practical difference shows up:

Dimension Monolithic workflow Modular sub-workflows
Debugging a broken step Scroll through 40+ nodes, find the one that failed Open the sub-workflow directly, run it with test data
Reusing logic across automations Copy-paste nodes, then maintain two copies separately One sub-workflow called from multiple parents
Execution count (Cloud plans) One execution per parent run Sub-workflow executions do not count toward plan limits
Testing a single piece Must run the whole workflow with test data Run the sub-workflow independently with its own test trigger
Making a change Risk of breaking downstream expressions Change the sub-workflow, all callers get the update
Canvas readability Gets worse with every new feature Parent stays a high-level map; detail lives in sub-workflows

The modular side does have real costs. Setting up the input schema takes five minutes instead of zero. Debugging a failure now requires you to check two canvases. And the single biggest gotcha: if the sub-workflow itself has a save error or a broken expression, the parent cannot call it at all.

The error handling gotcha you will hit exactly once

The n8n docs state this clearly: "If there are errors in the sub-workflow, the parent workflow can't trigger it." I learned this when I deployed a sub-workflow with a broken expression in an untested branch. The main automation had been running fine for two weeks. I updated the sub-workflow, introduced a typo in an expression, saved, and the whole parent automation failed silently at the Execute Sub-workflow node.

The fix is simple but you have to be disciplined about it. Before I connect a sub-workflow to a parent for the first time, I give it a Manual Trigger node alongside the Execute Sub-workflow Trigger, run it end-to-end with test data, and verify every output field. Then I disable or delete the Manual Trigger and connect the parent. When I update an existing sub-workflow I do the same: add a manual trigger, test, verify, remove, save. The two-minute test eliminates the silent-failure mode entirely.

For deeper error handling, I also wrap my Execute Sub-workflow node in n8n's standard error handling setup: an Error Trigger workflow that fires when any execution fails and sends me a Slack alert. But that is the same error handling I use on every workflow. Sub-workflows do not need anything special there.

Restricting which workflows can call a sub-workflow

The Execute Sub-workflow Trigger has a "Caller policy" setting in the workflow's Settings panel (not in the node itself). The options are:

In the JSON I showed earlier, I set "callerPolicy": "workflowsFromAList" in the settings block. For the lead-enrichment example that restriction makes sense: I want to know exactly which automations are calling the Clearbit endpoint.

A pattern I use constantly: the reusable notifier

The most-called sub-workflow I have built is a Slack notifier. It takes three fields: channel (string), message (string), and severity (string: "info", "warning", "error"). Inside the sub-workflow, a Switch node routes by severity to set the right emoji prefix, then a Slack node sends the message. That is it.

I now call this sub-workflow from nine different automations. When Slack changed their block-kit format for attachments last year, I updated one sub-workflow and all nine automations got the fix. Before I had this pattern, I had Slack nodes scattered across every workflow, each slightly different, and I had to hunt them all down one by one.

Other good candidates for sub-workflow extraction from my current setup:

The rule I use to decide whether something belongs in a sub-workflow: if I can name it as a single thing ("enrich lead", "score lead", "notify team"), it probably belongs in a sub-workflow. If the name would be "do everything in the middle", it stays inline.

Sub-workflows and the n8n automation hub

If you are building out a serious n8n setup and have not read through the pillar concepts yet, I keep a running guide on the n8n automation page that covers workflow architecture, the node types worth knowing first, and how I approach client builds. The expressions post from last week (covering how $json, $items, and the Code node interact) is also worth reading before you build complex sub-workflows: n8n Expressions and JavaScript in Workflows. And the webhook tutorial covers the other main trigger pattern you will want alongside sub-workflows once your automations start receiving data from external systems.

If your automations are already saving your team real hours every week and you want to know whether there is more to extract, the ROI calculator walks through the math for quantifying what your workflows are actually worth in saved hours.

What I would do differently starting over

Go modular from the start, even for "simple" automations. My rule now is: any workflow that will have more than 15 nodes gets broken into sub-workflows at the design stage, before I build anything. The 15-node threshold is not scientific. It is just where I have found the tipping point between "I can hold this in my head" and "I cannot."

I would also name sub-workflows with a prefix from day one. All of mine now start with "Sub: " so they sort together in the workflow list and I can immediately tell a sub-workflow from a parent when I am looking at the sidebar. A naming convention costs nothing and saves genuine confusion when you have 30 workflows.

The last thing I would change: I would define input and output schemas before writing a single node inside the sub-workflow. Agreeing on the interface first (what fields go in, what fields come out) means I never get halfway through the build and realize the parent is sending something the sub-workflow cannot use. It takes ten minutes with a pen and paper and it eliminates a whole category of rework.

Frequently Asked Questions

Do n8n sub-workflow executions count against my plan limit?

No. According to the n8n documentation (checked 2026-09-28), sub-workflow executions do not count towards your plan's monthly execution or active workflow limits. This makes sub-workflows especially practical for Cloud plan users who need to run complex automations without hitting their quota.

What is the difference between the Execute Sub-workflow node and the Execute Sub-workflow Trigger?

The Execute Sub-workflow node lives in the parent workflow and makes the call. The Execute Sub-workflow Trigger (labeled "When Executed by Another Workflow" on the canvas) lives in the sub-workflow and receives the call. You need both: the trigger defines the input schema in the sub-workflow, and the node in the parent sends data to it and waits for the result.

Can a sub-workflow call another sub-workflow?

Yes. n8n sub-workflows can be nested: a sub-workflow can itself contain an Execute Sub-workflow node and call a further sub-workflow. There is no hard documented depth limit, but deeply nested chains are harder to debug. I keep nesting to two levels in practice and flatten anything deeper.

What happens if my sub-workflow has an error?

If the sub-workflow contains errors, the parent workflow cannot trigger it at all. The execution fails at the Execute Sub-workflow node before any data is processed. This is why I always test a sub-workflow in isolation first by adding a manual trigger alongside the Execute Sub-workflow Trigger, running it with real test data, and only then connecting it to the parent.

How do I pass data back from a sub-workflow to the parent?

The sub-workflow's last node automatically returns its output items to the Execute Sub-workflow node in the parent. You do not need any special "return" node. Whatever JSON the final node outputs becomes the output of the Execute Sub-workflow node. I always use a Set node as the last node in the sub-workflow to define exactly which fields come back and keep the output shape predictable.

Ready to Get Started?

Book a free 30-minute discovery call. I will look at your current workflows and show you exactly where sub-workflow architecture would make the most difference in your setup.

Book a Free Discovery Call

Suyash Raj
Suyash Raj Founder, Voxdonna AI and AiSewak. Writes here about n8n, AI agents, and voice automation.