The same four nodes in five workflows: pull them into a sub-workflow
You have copy-pasted the same run of steps into five workflows. One small change now means five edits, and you are never quite sure you got them all. This part shows you how to lift shared logic out into a workflow of its own, call it from everywhere else with one node, and change it in a single place from then on. It is a function call, drawn on a canvas.
Copy-paste is a loan you repay every time something changes
By now you can read a workflow, branch it with IF and Switch, catch its failures with an Error workflow, and reshape its data with Edit Fields. You also have more and more workflows, and they are starting to look alike.
Then one day you open your own n8n home page and see it. Those four steps for “send a Slack notification” — an IF on severity, a Set for the title, a Slack node to send the message, a row appended to Sheets — are sitting inside five different workflows, identical in all five.
#alerts to #ops-alerts. Five places again.That is where copy-paste development always ends up. The fix is to pull those four steps out into a small standalone workflow — a sub-workflow — and have the other workflows call it with an Execute Workflow node. From then on, changing the shared logic means changing that one workflow, and every caller picks up the new version automatically.
You wrote the plumber's phone number on five sticky notes and put one in every room. It worked, right up until he changed number. Now you are hunting for sticky notes and hoping there were only five. A sub-workflow is writing it in the address book instead — one entry, one correction, and every room is up to date the moment you close the book.
A sub-workflow is not a special kind of workflow
There is no sub-workflow node type and no add-on to install. A sub-workflow is an ordinary workflow, just one designed to be called by others. The whole mechanism rests on two nodes playing two roles.
| Role | Which node to use | What it does |
|---|---|---|
| The caller (parent workflow) | Execute Sub-workflow — an action node; older interfaces show it as Execute Workflow |
Drop it in, pick the workflow to call, hand it items, and wait for the result — or do not wait |
| The callee (the sub-workflow) | Execute Sub-workflow Trigger — a trigger node that starts the workflow; older interfaces show it as Execute Workflow Trigger |
Catches the items handed in as its input. When the run finishes, the output of the workflow's last node is the return value |
Put together, that is a function call with the four familiar moves: the caller passes arguments, the sub-workflow runs on them, the last node's items come back as the return value, and the caller treats that return value as the Execute Workflow node's output and carries on downstream.
sequenceDiagram participant U as Node before the call participant C as Execute Sub-workflow participant T as Execute Sub-workflow Trigger participant L as Last node of the sub-workflow U->>C: items arrive from upstream C->>T: hands those items over as arguments T->>L: the sub-workflow runs its own nodes L-->>C: the last node's items come back as the return value C->>C: that becomes this node's output
def, JavaScript's function — a sub-workflow is n8n's version of one. Same input, processing, output; same value in reuse.Split too eagerly and you trade one mess for another
Not every piece of logic is worth splitting out. Do it too readily and your workspace fills with fragment workflows, which makes things harder to manage rather than easier. Run the question through this before you reach for a new workflow.
flowchart TD A["A run of logic you are
thinking of splitting out"] --> B{"Does the same logic appear
in three or more workflows?"} B -->|"yes"| S["Split it out"] B -->|"no"| C{"Is it ten or more nodes,
messy even to you?"} C -->|"yes"| S C -->|"no"| D{"Does one setting inside it
need managing in one place?"} D -->|"yes"| S D -->|"no"| E{"Does another team or colleague
need to call this logic?"} E -->|"yes"| S E -->|"no"| F["Leave it where it is"]
| Scenario | Split it out? | Why |
|---|---|---|
| The same logic, three nodes or more, appears in 3+ workflows | Split it | Copy-paste is a maintenance nightmare; split it and one change covers them all |
| A piece of logic is complicated — ten nodes or more — and even you find it messy | Split it | It becomes a black box; the parent is left with the high-level steps and reads cleanly |
| The logic is used in only one workflow | Leave it | Nothing needs sharing, and the split adds a jump that makes reading harder |
| Only one or two nodes — a single Slack message, say | Leave it | You save very little and gain another file to maintain |
| You want one piece of configuration managed in one place, such as a Slack channel name | Split it | When the channel is renamed you change the sub-workflow only, and every caller picks up the new value |
| Another team or a colleague needs to call your logic | Split it | A sub-workflow is the interface you hand other people — cleaner than letting them copy your workflow |
A shared “send a Slack alert” workflow, built once
Take one concrete case. The logic to split out is: take an event, decide the channel and the color from its severity — error, warn or info — and send it to Slack. After this, any workflow that needs to notify somebody calls this one instead of assembling Slack itself.
flowchart LR T["Execute Sub-workflow Trigger
input schema from a JSON example"] --> S{"Switch or IF
on the severity field"} S -->|"error"| E["Slack node
error channel, red"] S -->|"warn"| W["Slack node
warn channel"] S -->|"info"| I["Slack node
info channel"]
-
Step 1
Create the workflow and name it with a prefix
From the home page,
Workflows → + Create workflow. Click the workflow name in the top right and change it to[Sub] Slack Alert. The[Sub]prefix is a convention — the naming section below covers the rest of them — and it tells you at a glance that this workflow exists to be called and never runs on its own. -
Step 2
Use Execute Workflow Trigger as the trigger node
On the canvas press
Tab, or click+, to open the nodes panel. Search forExecute Workflow Triggerin the trigger family and drag it in. Its role is “when somebody calls me, I start here”. Without it, another workflow'sExecute Workflownode finds no entry point at all. -
Step 3
Set Input data mode to Define using JSON example
Open the
Execute Sub-workflow Triggernode and find the Input data mode dropdown. There are three options:Define using fields below(type each field name and type by hand),Define using JSON example(paste a sample and n8n infers the schema for you) andAccept all data(no validation — whatever arrives is taken). ChooseDefine using JSON exampleand paste this object:{ "severity": "error", "message": "Database connection failed", "source": "backup-daily" }Three keys, and each one earns its place:
Field Example value What the sub-workflow does with it severity"error"Chooses the branch, and therefore the channel message"Database connection failed"The body of the Slack message source"backup-daily"Says which workflow raised it, so a reader knows where to look The benefit is on the other side: the caller sees this schema hint in its own
Execute Sub-workflownode and can assemble its data to match, which saves the back-and-forth over “what am I supposed to send you”. A team that wants the contract written down first picksDefine using fields below; a team that would rather take anything and sort it out later picksAccept all data. -
Step 4
Add an IF node to test the severity
Wire in an
IFnode with the condition{{ $json.severity }}equalserror. The true branch goes to “post in the#ops-errorchannel with a red icon”. The false branch gets anotherIFforwarnandinfo— or you can split three ways in one go with aSwitchnode, which is what Switch is designed for. -
Step 5
One Slack node per branch, with the channel following the severity
Give each branch its own
Slack → Message → Sendnode. The error branch's Channel is#ops-error, warn takes#ops-warn, and info takes#ops-info. Build the Text field from one shared expression in all three:[{{ $json.severity | upper }}] {{ $json.source }}: {{ $json.message }}All three channels then get the same-looking message, and you maintain that format in one place.
-
Step 6
Let the workflow end on its own
Let each of the three Slack nodes be the end of its branch. When a sub-workflow finishes, the last node's items are sent back to the caller as the return value automatically. Do not add a
Respond to Webhooknode — that one belongs to theWebhooknode, not to sub-workflows. If you want to return a structure of your own instead, put aSet/Edit Fieldsnode at the end of each branch and build it there, for example{ "notified": true, "channel": "#ops-error" }. -
Step 7
Save, and note the workflow ID from the address bar
Save with
Ctrl + S, then look at the browser address bar. It reads something likehttps://n8n.woowtech.io/workflow/abc123XYZ, and that last segment is the workflow ID. If the caller's Workflow dropdown cannot find this workflow later, you can paste the ID straight in. One thing not to do here: a sub-workflow does not need its Active toggle switched on, because it never runs from a trigger of its own. It waits to be called. -
Step 8
Test it once from the trigger node
Click Execute step in the top right of the
Execute Workflow Triggernode. It does a trial run using the data you pasted into the JSON example as its input. Of the three Slack nodes, only the error branch should fire, because the example's severity iserror. A Slack message actually arriving means the sub-workflow itself is sound.
Delete the four nodes, drop in one
The sub-workflow is built. Now open an existing workflow — “back up the database daily”, say — take out the Slack notification nodes stuffed inside it, and replace them with a single Execute Workflow node.
-
Step 1
Open the workflow you are reworking
Say you have a workflow that backs up the database in the small hours: a
Schedule Trigger, then the backup, then anIFon the exit code, then a Slack message each for success and failure. It is that last stretch you are about to take apart. -
Step 2
Delete the old Slack nodes and drag in Execute Workflow
Delete your own IF, Set and Slack nodes entirely. Search the nodes panel for
Execute Workflow— the action one, not the trigger version, which is easy to grab by mistake — and wire it in after theIFnode. -
Step 3
Set Source to Database and pick the sub-workflow
Open the node. Source has four modes:
Database(pick from the workflows already in the workspace, the usual choice),Local File(read a JSON file from the machine),Parameter(paste the whole workflow JSON into the node) andURL(point at a workflow URL). Keep the defaultDatabaseand find[Sub] Slack Alertin the dropdown. If it is not there, switch to one of the other modes and point at it by hand. -
Step 4
Decide on Wait for Sub-workflow Completion
This toggle decides whether the caller waits for the sub-workflow to finish and hand back a return value before carrying on. On means wait; off means carry on the moment the call goes out, which suits a pure notification where you do not care about the result. If you want to keep working with the data the sub-workflow returns, it has to be on. The official docs do not state the default; in practice a newly created
Execute Sub-workflownode has it on, so check it yourself if you want to be sure. -
Step 5
Set Mode to Run once with all items
Mode has two options.
Run once with all itemshands every upstream item to the sub-workflow at once and the sub-workflow runs a single time.Run once for each itemtriggers the sub-workflow separately per item, so it runs N times. For a one-at-a-time case like a notification,Run once with all itemsis fine. -
Step 6
Fill in what you pass, or reshape it first
If the sub-workflow defined a schema on its trigger — either
Define using fields beloworDefine using JSON example— theExecute Sub-workflownode automatically shows a Workflow Inputs table where you fill in an expression per field, plus an Attempt to convert types toggle that handles type conversion for you. If the sub-workflow is onAccept all data, that block does not appear and the upstream items go in untouched. To change the structure substantially, add aSet/Edit Fieldsnode in front and build the three fields there:severity: errormessage: {{ $json.error_message }}source: backup-dailyThen save and run it once by hand with Execute Workflow to verify three things in one go: the sub-workflow wakes up, Slack gets the message, and the
Execute Workflownode on the caller's side shows the items that came back.
Execute Workflow. It reads far more cleanly. And when your boss later says the Slack notification needs an emoji, you change the sub-workflow only and every caller picks it up.Two ways to pass parameters, and one to avoid by default
How does the Execute Workflow node decide what to hand over? There are three settings worth knowing, and the third one is the one that bites.
| Mode | How to set it | When to use it |
|---|---|---|
| Items pass-through | Set Mode to Run once with all items and change nothing else — the upstream items become the sub-workflow's input directly | The upstream items already have the shape the sub-workflow expects, both {severity, message}, say |
| Reshape with a Set node first | Add a Set (or Edit Fields) node in front of Execute Workflow and rewrite the items into the schema the sub-workflow expects | The upstream data does not match — different field names, missing fields, extra fields. This is the normal case |
| Run once for each item | Set Mode to Run once for each item | You genuinely want a separate sub-workflow call per item: 100 notifications means 100 sub-workflow runs |
Run once for each item. The sub-workflow is called N times, and the Executions page fills with N sub-workflow execution records. A large loop slows the whole thing down and eats the quota. For a genuinely large batch, batch it first and then hand it over in one go with Run once with all items.Wait on means phoning the workshop and staying on the line until they tell you the part is in. Wait off means leaving a message and getting on with your day. Both are reasonable. Staying on the line for five minutes when all you wanted was to leave a message is not.
sequenceDiagram participant P as Parent workflow participant S as Sub-workflow Note over P,S: Wait for Sub-workflow Completion, on P->>S: call, with items S-->>P: returns items when it finishes P->>P: downstream nodes run, using what came back Note over P,S: Wait for Sub-workflow Completion, off P->>S: call, with items P->>P: downstream nodes run straight away S->>S: finishes on its own, return value goes nowhere
Six limits, a prefix convention, and a starter toolbox
Splitting logic out feels good. A handful of practical limits are worth knowing now rather than tripping over them six months from now.
| Item | What actually happens | What we suggest |
|---|---|---|
| Executions are counted separately | One caller run is one execution and one sub-workflow run is another. The Executions page lists the two sides separately | Tracing one complete process means cross-referencing both sides; set an Error workflow on both as well |
| A call chain that runs too deep bogs down | A sub-workflow calling a sub-sub-workflow is fine, but many levels get slow and hard to debug | In practice, do not go past 3-4 levels. If it really is that deep, redesign the process or collapse a few levels into one |
| Version control | Changing a sub-workflow immediately affects every caller. There is no rollback button and no branch | Before you change it, Duplicate workflow and save the copy as [Sub] Slack Alert - backup 20260816, or back it up by exporting the JSON |
| If the sub-workflow returns nothing, the caller gets nothing | If the last node is an IF and one branch has no node wired after it, a run down that branch leaves the caller with [] | Every path in the sub-workflow needs a real end node — a Set node producing a result object counts |
| The Wait option cuts both ways | Wait for Sub-workflow on means the caller waits; if the sub-workflow takes five minutes, the caller is stuck for those five minutes too | For pure notifications, where the return value does not matter, turn Wait off so the caller moves on the moment it calls |
| An error inside the sub-workflow surfaces in the caller | A node failing inside the sub-workflow turns the whole Execute Workflow node red, and the caller's workflow fails with it | To have the caller ignore sub-workflow errors, set the Execute Workflow node's On Error to Continue |
One errand, two receipts. You paid at the counter and the counter paid the supplier, and the accounts hold both slips separately. Nothing is wrong with that — but when you go looking for what a purchase cost, you have to pull two pieces of paper, and knowing that in advance saves the ten minutes you would spend assuming one of them is missing.
Prefixes, so you can tell them apart at a glance
After a while a workspace holds dozens or hundreds of workflows, half of them parents and half of them called by others. All mixed together, you cannot find anything. Sorting by prefix is the simplest thing that works.
| Prefix | What it means | Example |
|---|---|---|
[Sub] | A sub-workflow other workflows call, possibly several of them | [Sub] Slack Alert, [Sub] Format Local Date |
[Util] | Pure utility, always used by a sub-workflow or internally, never exposed | [Util] Escape Slack Markdown |
[Prod] / [Dev] | Keeps the live version and the test version apart when both exist | [Prod] Daily Backup, [Dev] Daily Backup |
[Cron] | A scheduled workflow started by a Schedule Trigger | [Cron] Hourly CRM sync |
[Webhook] | A workflow triggered by an outside system calling in | [Webhook] LINE bot reply |
| No prefix | Something you are still testing by hand, or a draft | Test new API |
The workflow list page supports search, so typing [Sub] brings up every sub-workflow at once, which makes both editing and taking stock easy. WoowTech's own team has used this convention for a long time, and a new colleague gets it in a second.
A starter toolbox of four
Once the idea has landed, you can build your team a shared toolbox. Here are four sub-workflows plenty of companies actually run, for reference.
| Sub-workflow | What it does | Input schema | Output |
|---|---|---|---|
[Sub] Slack Alert | Sends every Slack notification, routing by severity to a different channel | { severity, message, source } | { notified: true, channel: '...' } |
[Sub] Log Execution | Writes which workflow ran when, whether it succeeded or failed, and how long it took into a shared Google Sheet | { workflow_name, status, duration_ms, note } | { logged: true, row_id: 42 } |
[Sub] Format Local Date | Converts any date or time to the organization's chosen timezone in the standard YYYY-MM-DD HH:mm:ss format. Configure the IANA timezone for your own location | { raw_date }, ISO or a Unix timestamp | { formatted: '2026-08-16 14:23:00' } |
[Sub] Get Employee Info | Looks up the internal HR API by email or employee number and returns the person's name, department and manager | { email } or { employee_id } | { name, department, manager_email } |
With those four built, everything else gets cleaner. Need to notify? Do not assemble Slack yourself — call [Sub] Slack Alert. Need a log? Call [Sub] Log Execution. Need to show a time to a person? Call [Sub] Format Local Date and the whole company shows one format. Need to look up a colleague? Call [Sub] Get Employee Info, and when the internal API changes URL you edit exactly one workflow.
Later the HR API address changes, the Slack channel is renamed, the timezone format is adjusted — you edit only the matching sub-workflow, and every workflow in the company picks up the new version automatically. That is what sub-workflows are really worth. The first one costs you time to build and an interface to explain to colleagues — what to send, what comes back — but every extra caller doubles the return on the set again.
Which side is broken, the caller or the callee
Two workflows means two places a fault can live, and the Executions page will not join them up for you. Work out which side you are on first.
flowchart TD A["The call did not do
what you expected"] --> B{"Can the Workflow dropdown
even find the sub-workflow?"} B -->|"no"| B1["Check it still exists on the Workflows page,
or point at it by hand with
URL, Local File or Parameter"] B -->|"yes"| C{"Does the run say the
sub-workflow could not be started?"} C -->|"yes"| C1["The first node is not an
Execute Sub-workflow Trigger.
Fix the entry point"] C -->|"no"| D{"Did items actually arrive
at the Execute Workflow node?"} D -->|"no"| D1["Nothing upstream, or the wrong Mode.
The sub-workflow was never called"] D -->|"yes"| E{"Did the sub-workflow
return empty items?"} E -->|"yes"| E1["A branch inside it ends on nothing.
Give every path a real end node"] E -->|"no"| F["Both sides ran. Open the two
executions and compare by time"]
| Symptom | Cause | How to fix it |
|---|---|---|
The Workflow dropdown on the Execute Sub-workflow node cannot find the target | Wrong workflow ID, the sub-workflow was deleted, or you are in a different workspace | Check on the Workflows page that it is still there. Or switch Source from Database to URL, Local File or Parameter and point at it by hand |
The run reports Sub-workflow could not be started | The sub-workflow's trigger node is not an Execute Sub-workflow Trigger — it is a Manual or Schedule trigger, say | Go back to the sub-workflow and make an Execute Sub-workflow Trigger node its first node. That is the entry point sub-workflows use |
| The caller passed items but the sub-workflow received nothing | The Execute Workflow node's Mode is set wrong — Run once for each item selected, say, but there are no upstream items | Add a node upstream to watch whether items really show up, and confirm the Mode. If upstream is empty, the sub-workflow is never called at all |
| The sub-workflow is extremely slow and the caller is stuck behind it | Wait for Sub-workflow Completion is on and the sub-workflow itself takes a long time; or the caller used Run once for each item and looped N times | For a pure notification, turn Wait off. To run things side by side, switch to a batch plus Run once with all items. If it really is slow, go find the bottleneck inside the sub-workflow |
| The sub-workflow fails and the caller's whole workflow blows up with it | The Execute Workflow node's On Error defaults to Stop Workflow, so an error inside surfaces in the caller | If the caller should carry on regardless — the Slack notification fails but the main process is unaffected — change On Error to Continue |
| One of the callers broke after you changed the sub-workflow | You touched the input schema it expects — renamed a field, added a required one — and that caller was not updated with it | The Input data mode on the trigger is the interface contract. Take stock of every caller before changing it. For a breaking change, build a separate [Sub] Slack Alert v2 and run both through the transition |
| Something failed and you cannot tell whether it was the sub-workflow or the caller | The Executions page lists caller and callee as two separate records and does not string them together for you | Open the Execute Sub-workflow node inside the caller's execution — the returned output usually carries the matching sub-execution link or ID — or cross-reference by time on the Executions page. If that leaves you uneasy, set an Error workflow to collect them in one place |
| The sub-workflow returns something but the caller sees empty items | A branch off the last node has nothing connected to it — one leg of an IF left dangling, say | Make sure every branch inside has a real end node. If you want a branch to do nothing but still return something, wire in a Set node with { done: true } |
The credential question catches people out, so here it is in one line before the questions below. The sub-workflow runs on its own franking machine. Whoever hands it a letter, the stamp on the envelope is the company's, charged to the company's account. Nobody handing over a letter has to know the account number, and nobody can accidentally send one on their own.
Can a sub-workflow call another sub-workflow?
Execute Workflow node that calls another one, forming an A → B → C chain. But many levels get slow and hard to debug — every level is its own execution, so the Executions page shows three records and tracing one failure takes three jumps. In practice, keep it within 3-4 levels; past that, redesign, or merge the middle levels.Do sub-workflows have version control? Can I roll back a bad change?
[Sub] Slack Alert - backup 20260816. Two, export the JSON into Git: ⋯ → Download at the workflow's top right gets you the JSON, and dropping that into your own Git repo gives you the full version history. n8n Cloud and Enterprise ship workflow history and versioning you can use directly.What happens when two people edit the same sub-workflow at once?
[Dev] copy first and copy them back to [Prod] once tested; and note that the Enterprise edition has role-based permissions that genuinely separate read-only from writable.Can I disable a sub-workflow? What happens to the callers?
Execute Workflow Trigger node — and calls to it then fail: the caller's Execute Workflow node reports that the sub-workflow cannot be found, or errors out. If you have set an Error workflow, it receives that failure and tells you. The way to switch a piece of logic off temporarily without breaking anyone: put an IF node at the start of the sub-workflow that checks a flag, and when the flag is false send it down an empty branch that does nothing. The callers keep working; the sub-workflow effectively does nothing.Sub-workflow or Code node — when do I use which?
Code node, which the next part covers, is a few lines of JavaScript inside a single node to work on data. A sub-workflow is a whole stretch of logic across nodes that can be reused in several places. Four ways to decide: if you only need to transform, filter or calculate data, the Code node is faster; if the logic itself involves several node calls, Slack plus Sheets plus a test, use a sub-workflow; if you want non-technical colleagues to be able to read the logic, use a sub-workflow, because it is visual; and if the logic has to be reused across several workflows, either works, but a sub-workflow suits maintenance by non-developers better.Which credential does a sub-workflow run with — the caller's or its own?
[Sub] Slack Alert is bound to the company Slack workspace, it makes no difference who calls it or what account the caller is bound to — the message always goes out through the company workspace. That is a good thing: the sub-workflow is self-contained and the caller does not have to care about its authentication. It is also why enterprise setups often bind a shared set of sub-workflows entirely to a service account, so nothing breaks when someone leaves.Do sub-workflows have a timeout?
EXECUTIONS_TIMEOUT environment variable — on Woow n8n the default is usually 3600 seconds, one hour. Past that it is forcibly stopped and the caller sees the execution fail. If the sub-workflow is long-running by nature, fetching data from an outside API for instance, weigh up splitting it into an asynchronous pattern: the caller triggers it, does not Wait, and the sub-workflow logs or notifies on its own once it finishes. That keeps the caller from being stuck.Can I turn a sub-workflow into a public API for other systems to call?
Execute Workflow Trigger can only be called by other workflows in the same workspace; an outside system cannot reach it. To let outside systems call in, start with a Webhook node instead. The common wrapper pattern: a [Webhook] Slack Alert API workflow uses a Webhook as its entry point, runs an auth check, and then uses Execute Workflow to call the internal [Sub] Slack Alert. The same logic is then reached internally through the sub-workflow and externally through the webhook, over one implementation. Do not paste real keys or a real webhook URL into a workflow you intend to share — strip credentials out before exporting the JSON.Where to go from here
You can reuse a whole stretch of logic. Next, a few lines inside one node.
Part 15 covers the Code node: when a handful of JavaScript beats another five nodes on the canvas, and where the line between the two sits in practice.
Part 14 of the Woow n8n Onboarding Guide series on the Apporo blog.
Adapted from the Woow n8n Onboarding Guide, produced by WoowTech and released under CC BY 4.0. This adaptation is published by Apporo under the same licence.
Light · Air · Water · Control · apporo