n8n tutorial - Lesson 21: Sub-Workflows in n8n: Build Modular Automation
Hi everyone, in this n8n sub workflow tutorial, you'll learn how to split a large automation into reusable child workflows using the Execute Workflow node. This is a core pattern in n8n workflow automation that keeps your projects modular, maintainable, and easy to scale.
How to do:
Step 1 — Understand When to Use Sub-Workflows
Before building anything, know the five situations where the sub-workflow pattern pays off in n8n workflow automation.- Use a sub-workflow when the same logic is called from multiple parent workflows — for example, a "post to Blogger" routine used by both a blog factory and an email pipeline.
- Use it when a single workflow exceeds ~15 nodes and becomes hard to read or debug.
- Use it when you want to test one processing unit (text formatting, image generation, number math) independently without running the full parent.
- Use it when different team members own different stages — each person edits their own child workflow without touching shared logic.
- Use it when you need to process each item in a loop through a consistent set of nodes — the parent iterates, the child does the work.
Step 2 — Build Child Workflow A (Text Processor)
The first child workflow,T6-B2-Child-A-TextProcessor, receives a text string, transforms it to uppercase, and returns the result.
- Create a new workflow named
T6-B2-Child-A-TextProcessor. - Add a When Executed by Another Workflow trigger node. In the Input Schema, define one field: name
text, type String. -
Add a Set node after the trigger. Configure three output fields:
result→ expression{{ $json.text.toUpperCase() }}processedAt→ expression{{ $now.toISO() }}processedBy→ fixed stringChild-A-TextProcessor
- Save the workflow, then click Test workflow with pinned data
{"text": "hello sub workflow"}. Expected output:result: "HELLO SUB WORKFLOW".
Note — Depending on your n8n version, the When Executed by Another Workflow trigger's schema screen may only show Name and Type fields — there is no auto-generated sample value input box. You must pin test data manually to run standalone tests.
Step 3 — Build Child Workflow B (Number Processor)
The second child,T6-B2-Child-B-NumberProcessor, receives a number and returns its square — demonstrating that each child can use a completely different node type.
- Create a new workflow named
T6-B2-Child-B-NumberProcessor. - Add a When Executed by Another Workflow trigger. Define one schema field: name
number, type Number. -
Add a Code node with the following JavaScript:
const n = items[0].json.number;return [{ json: { squared: n * n, processedAt: new Date().toISOString(), processedBy: "Child-B-NumberProcessor" } }];
- Save and test with pinned data
{"number": 7}. Expected output:squared: 49.
Step 4 — Build the Parent Dispatch Workflow
The parent workflow,T6-B2-Parent-Dispatch, sends data to both children in parallel and merges the results.
- Create a new workflow named
T6-B2-Parent-Dispatch. - Add a Manual Trigger node, then a Set node to mock input — set
texttohello sub workflowandnumberto7. -
From the mock input node, create two parallel branches:
- Branch 1: Execute Workflow node → select
T6-B2-Child-A-TextProcessor. Set Run once for all items. Maptext→{{ $json.text }}. - Branch 2: Execute Workflow node → select
T6-B2-Child-B-NumberProcessor. Set Run once for all items. Mapnumber→{{ $json.number }}.
- Branch 1: Execute Workflow node → select
- Add a Merge node connected to both branches. Set Mode to Combine By Position.
- Run the parent and verify the merged output contains fields from both children:
result,squared, and bothprocessedAtvalues.
Tip — Use Combine By Position (not Combine By Matching Fields) when your two branches don't share a common key field. Combine By Matching Fields requires a shared identifier — in this demo, there is none.
Step 5 — Know the 7 Gotchas Before Going Further
These are the practical issues you will hit in real n8n sub workflow builds.- Field name collision: If Child A and Child B both output a field called
processedAt, the Merge node will overwrite one with the other. Rename fields to be unique per child (e.g.,processedAt_AvsprocessedAt_B). - Timezone offset:
$now.toISO()returns local time;new Date().toISOString()in a Code node returns UTC. Be consistent across children to avoid confusion in logs. - Cross-node reference breaks after deleting a node: If your child was duplicated from another workflow and still references a deleted node — e.g.,
$('Get Topics')— the expression will throw an error. Update every reference to point to the new trigger:$('When Executed by Another Workflow'). - Double-equals bug: In the Execute Workflow input mapping, if the field is already in Expression mode, type
{{ $json.topic }}— do NOT prefix it with=. Writing={{ $json.topic }}passes the literal string=Hướng dẫn...as the topic title instead of the actual value. - Schema only validates, it does not inject sample data: The trigger schema defines expected input types. It does not create a test input form — pin data manually for standalone testing.
- Run mode matters: Run once for all items calls the child once and passes all items in a batch. Run once for each item calls the child once per item. Use "each item" when the child must process one topic at a time (e.g., the Content Factory).
- On Error behavior: Each Execute Workflow node has an On Error setting with three options: Stop Workflow (default — halts everything), Continue (skips the failed item silently), Continue (using error output) (routes the error to a separate output pin for handling). For production, set this to Continue (using error output) so one bad topic doesn't kill the whole batch.
Step 6 — Refactor Content Factory into Parent + Child
Now apply the sub-workflow pattern to a real project: the existingT4-B5-Blog-Batch workflow (14 nodes) is split into a parent dispatcher and a child blog generator.
-
Design the split:
- Parent (
T6-Content-Factory-Dispatch): reads topics from Google SheetT4-B5-Blog-Topics, calls the child for each topic, marks the sheet row as done. - Child (
T6-Content-Child-Blog): receives onetopicstring, generates HTML blog post, posts draft to Blogger, returnstopic/status/postId/blogUrl.
- Parent (
-
Build the child workflow:
- Duplicate
T4-B5-Blog-Batch. Rename itT6-Content-Child-Blog. - Delete the Get Topics, Limit, and Mark Done nodes.
- Add a When Executed by Another Workflow trigger with schema field
topic(type String). - Find every expression that references
$('Get Topics')and replace it with$('When Executed by Another Workflow'). - Add a final Set node (Build Output) that returns:
topic,status,postId(from$json.idof the Blogger POST response),blogUrl(from$json.url).
- Duplicate
-
Build the parent workflow:
- Duplicate
T4-B5-Blog-Batch. Rename itT6-Content-Factory-Dispatch. - Delete the 11 internal processing nodes, keeping only: Manual Trigger → Get Topics → Limit → Mark Done.
- Insert a Call Child Blog (Execute Workflow) node between Limit and Mark Done. Select
T6-Content-Child-Blog. Set Run to Run once for each item. - Map the child input: field name
topic, value{{ $json.topic }}(no leading=). - In the Mark Done node, set the match condition to
topic = {{ $json.topic }}— this uses thetopicfield returned by the child output, ensuring the correct sheet row is marked regardless of item ordering.
- Duplicate
Production tip — Always match Mark Done using a value from the child's output (not the parent's pre-call data). If the child transforms or normalizes the topic string, matching against the child's returned topic field prevents row-pairing mismatches in your sheet.
Step 7 — Test the Full Content Factory and Fix Errors
Run the parent with one topic in the sheet and work through the three errors that appear in a typical first run.-
Error 1 — DALL-E
response_formatknown issue:- The OpenAI DALL-E node throws an error related to the
response_formatparameter on certain n8n versions. - Workaround: disable the DALL-E node and its 3 downstream image-handling nodes in
T6-Content-Child-Blog. The blog post will be created without an image until this is fixed in Session 22.
- The OpenAI DALL-E node throws an error related to the
-
Error 2 — Double-equals in topic title:
- Symptom: the blog post title appears as
=Hướng dẫn...(the raw expression string, not the evaluated value). - Fix: open the Call Child Blog node, find the
topicinput field, confirm it is already in Expression mode, and change the value from={{ $json.topic }}to{{ $json.topic }}.
- Symptom: the blog post title appears as
-
Error 3 — Broken cross-node reference:
- Symptom: child workflow throws
Referenced node does not exist: Get Topics. - Fix: search the child for every expression containing
$('Get Topics')and replace with$('When Executed by Another Workflow').
- Symptom: child workflow throws
- After all three fixes, run the parent again. Expected results:
- Blogger draft created with clean title ✅
- Google Sheet row marked as done ✅
- No expression errors ✅
Note — The DALL-E issue is a known bug in specific n8n versions. The fix — replacing the OpenAI node with an HTTP Request node calling images/generations directly without the response_format parameter — is scheduled for the next session.
Key Lessons from This Session
- Always test each child workflow standalone before connecting to the parent. Pin sample data to the trigger and verify output before wiring the Execute Workflow node in the parent.
- Use Combine By Position when branches share no common key field. Combine By Matching Fields requires a shared identifier — if none exists, the merge will silently drop rows.
- The double-equals bug (
={{ }}) passes a literal string, not an expression. If an input field is already in Expression mode, write{{ $json.field }}without the leading=. - Cross-node references break when the referenced node is deleted. After duplicating a workflow and removing nodes, audit every expression for references to deleted node names.
- Match Mark Done using the child's output topic, not the parent's pre-call data. This prevents sheet row mismatches when items are processed out of order.
- Set Execute Workflow error handling to "Continue (using error output)" in production. This routes failed items to a separate pin instead of stopping the entire batch.
- Run once for each item vs. run once for all items controls batching behavior. Use "each item" when the child must handle one record at a time with its own context.
Conclusion:
In this n8n sub workflow tutorial, you built a full three-workflow demo — two standalone child processors and a parent dispatcher — then applied the same pattern to a real Content Factory by refactoring a 14-node monolith into a clean parent-plus-child architecture. The sub-workflow pattern is the foundation for orchestrating multi-format content pipelines in n8n tutorial series like this one, and every production technique covered here (error routing, expression mode awareness, cross-node reference auditing) will carry forward into more advanced n8n workflow automation builds. Next session covers re-enabling the DALL-E image branch with a direct HTTP Request workaround, verifying postId and blogUrl output values, and expanding the factory to dispatch to YouTube and Email child workflows.
If you have any questions, feel free to leave a comment below. Thank you!
Tags: n8n sub workflow tutorial, n8n tutorial, n8n workflow automation, Execute Workflow node, modular automation, n8n Content Factory, n8n beginner to advanced, workflow design patterns
Maybe you are interested!
- Getting Started with n8n: Interface & Your First Manual Trigger
- n8n HTTP Request Node: Connect Any API Without Code
- Branching Workflows: IF, Switch & Merge Nodes in n8n
- n8n Expressions & Built-in Variables: The Complete Guide
- Comparing AI Models in n8n: Claude vs Gemini vs ChatGPT
- Connect Gmail to n8n: OAuth Setup & Reading Emails
- Build an AI Email Classifier with n8n
- Automated Email Digest to Telegram with n8n
- Google Sheets Automation with n8n: 4 Key Operations
- Auto-Generate Google Docs from Data with n8n


