Module 3: Contracts Between Workflows

4. The Execute Sub-workflow node's boundary

Description

By the end of this lesson you'll be able to build the real call between two workflows in n8n 2.0: add the node that makes order-triage invoke check-credit, understand what data crosses the boundary in each direction, and declare your schema's input fields inside the sub-workflow so n8n recognizes them. You're going to know which execution mode to choose based on what you pass it, and why it's worth a sub-workflow having a single entry point. You're also going to learn the execution model —how the parent workflow pauses, the sub-workflow runs, and the parent continues with the result—.

This matters because up to now the contract lived on paper: you defined it (lesson 2) and designed it as a schema (lesson 3), but you didn't put it to work. The Execute Sub-workflow node's boundary is where the contract stops being a document and becomes a real call that executes. Everything you validate (lesson 5) and version (lesson 6) happens around this boundary; without understanding well what crosses here and how, those two lessons stay up in the air.

Connection to the module: lesson 3 designed check-credit's schema on paper. This one transcribes it into the Execute Sub-workflow Trigger node and builds the call from order-triage. The fields you declare here are the ones lesson 5 is going to validate more deeply, and the ones lesson 6 is going to version. A note on the name: in n8n 2.0 the node that makes the call is called Execute Sub-workflow —in earlier versions it was called "Execute Workflow," and that's how you'll see it in old tutorials and some ecosystem guides—. The concept is identical; only the label changed. Since n8n's labels change from version to version, whenever a field or button's exact name matters I'll tell you and ask you to verify it on your own panel.

A single entry point: the door with a doorman

Think of a serious office building. You don't come in through the window, or the parking lot, or the service door. You come in through reception, where there's a doorman who checks who you are, what you're there for, and whether you're carrying what you need to be let through. That single entry point isn't a bureaucratic annoyance; it's what makes the building governable. Since everyone comes in through the same place, there's a single spot to check, a single spot to keep a log, a single spot to protect. A building with five uncontrolled entrances is a building nobody can secure.

A sub-workflow with a contract is that building, and the Execute Sub-workflow Trigger node is its reception. It's the sub-workflow's first node, the only one through which a call from another workflow comes in. It doesn't matter who calls —order-triage, another workflow, an agent—: everyone comes in through the same door, the one that declares which fields it expects to receive. That single entry point is what makes the contract governable: there's a single place to declare the schema (this lesson), a single place to validate the input (lesson 5), a single place to know which contract version you're serving (lesson 6).

Compare it with the messy alternative: a sub-workflow that assumes the data "already arrives fine" from wherever it was called, with no clear door. That sub-workflow has nowhere to put the doorman. Every caller could send it different things, and there's no single point to check. The boundary —the Execute Sub-workflow Trigger as the single reception— is the precondition for everything else. Without it, there's nowhere to stop whoever doesn't comply with the contract.

The boundary's two pieces

A call between workflows has two nodes, one on each side of the boundary. It's worth having them clear because they're easy to confuse —both have "Sub-workflow" in the name—.

On the responder's side: the Execute Sub-workflow Trigger. It's check-credit's first node. On the canvas, n8n shows it labeled "When Executed by Another Workflow," which describes exactly its role: this node fires when another workflow calls it. It's the reception seen from inside the building: where the visitor arrives. This is where you declare what fields your sub-workflow expects.

On the caller's side: the Execute Sub-workflow node. It's a node you put in order-triage, at the point where you want to invoke check-credit. It's the visitor showing up at the other building's reception. Here you choose which sub-workflow you're calling and what data you send it.

Together they form the boundary. Lesson 1's diagram now has real names:

order-triage (the caller)                  check-credit (the responder)
──────────────────────────                 ──────────────────────────────
[Webhook]                                   [Execute Sub-workflow Trigger]
   │                                         (canvas label:
[AI Agent]                                    "When Executed by Another Workflow")
   │                                                │
[Execute Sub-workflow] ─── input ──►          (query credit, compare, decide)
   │           ◄────────── output ─────────  [last node returns the result]
[HTTP Request to CRM]

On the responder's side: declaring the schema in the trigger

This is where the schema you designed in lesson 3 enters n8n. The Execute Sub-workflow Trigger node has an option —worth checking on your panel, because its label can vary— called "Input data mode," with three options. Each is a different way of telling n8n what your sub-workflow expects to receive.

"Define using fields below." You choose this option and n8n lets you list fields one by one: for each one, a name and a type (string, number, boolean, or json). It's a direct transcription of your schema. For check-credit, you list customer_id as string, order_id as string, amount as number, and currency as string. The power of this mode: when another workflow adds an Execute Sub-workflow node and chooses to call check-credit, n8n automatically shows these fields in the caller's node, already ready to fill in. The schema you declared once, at reception, shows up as a guide for every visitor. It's the contract actively helping people comply with it.

"Define using JSON example." You choose this option and, instead of listing fields one by one, you paste an example object —like the one you built at the end of lesson 3— and n8n infers the schema from it: it sees "amount": 1842.50 and infers amount is a number. It's faster if you already have a representative example, and it's why that concrete example you wrote for documentation also serves for declaring.

"Accept all data." You choose this option and the sub-workflow accepts whatever it's sent, with no declared expected field. It's the building with no doorman: anyone comes in with whatever they're carrying. It has its place —a generic utility sub-workflow that truly processes any item—, but for a sub-workflow with a contract like check-credit it's exactly what we don't want: it gives up the door that makes the contract governable. If your sub-workflow has a serious contract, this isn't your mode.

For check-credit, the choice is clear: "Define using fields below," with your schema's four fields. That way the reception knows who to let through and with what, and the callers get the guide of what to send.

An important nuance, connecting to lesson 5: declaring the fields in the trigger tells n8n the names and types you expect, and helps the callers, but it doesn't deeply validate business rules. The trigger knows amount should be a number; it doesn't know a negative amount makes no sense, or that customer_id should exist in your customer database. That deeper validation is next lesson's topic. For now, hold on to this: the trigger is the schema declaration; the full validation gets built on top.

On the caller's side: invoking the sub-workflow

Now let's cross over to order-triage, where you add the Execute Sub-workflow node at the point where you want to check the credit. This node has some parameters worth understanding, because each one is a decision about how it crosses the boundary. The exact names can vary between versions —check them on your panel—, but the concept behind each is stable.

Source. Where the sub-workflow you're going to call comes from. The normal option is choosing it from your list of workflows on the same instance ("Database" / "From list"). There are other sources —a file, a parameter, a URL— for advanced cases, but to call a sub-workflow of yours living on the same instance, you choose your list and select check-credit.

Workflow. Which sub-workflow. Here you choose check-credit from the dropdown. If in check-credit you declared the fields with "Define using fields below," this is the moment n8n shows you those fields ready to fill in —the contract's guide appearing on the caller's side—.

The inputs (the data you pass it). Here you fill in the fields the sub-workflow declared: what value goes in customer_id, which in order_id, which in amount. These values come from the data order-triage already has at that point —for example, the customer_id from the order that arrived through the webhook—. It's the visitor filling out the reception's form with their data.

Mode. How the call runs relative to the items arriving at the node. It has two options, and the choice matters:

  • "Run once with all items." The sub-workflow gets called a single time, and receives all the node's items at once. Useful when the sub-workflow is designed to process a complete batch.
  • "Run once for each item." The sub-workflow gets called once per item arriving at the node. If order-triage received three orders, check-credit runs three times, once per order, each with its own input and its own output.

For check-credit, the natural choice is "Run once for each item": check-credit's contract is written for one order —one customer_id, one order_id, one amount—, so you want it to run once per order. This decision isn't cosmetic: it changes the shape of the input the sub-workflow receives, and therefore how you write its validation in lesson 5. A contract designed for one item and called in "all items" mode is a classic source of confusion.

"Wait for Sub-Workflow Completion." A node option that, when active —which is normal—, makes order-triage stop and wait for check-credit to finish and return its result before continuing. It's what you want almost always: order-triage needs the approved to decide, so it has to wait for it. Turning it off would make order-triage fire the call and continue without waiting for a response —useful for tasks that don't need a result, but not for our case, where the result is precisely the point—.

The execution model: pause, execute, continue

It's worth seeing in slow motion what happens when order-triage reaches the Execute Sub-workflow node, because understanding this sequence keeps you from getting confused about why the workflow "sits there waiting" or where the data that continues comes from.

  1. order-triage reaches the Execute Sub-workflow node with an order in hand (an item with customer_id, order_id, amount).
  2. order-triage pauses. Since the wait option is active, the parent workflow sits still at that point. It doesn't move on to the HTTP Request to the CRM yet.
  3. check-credit runs. The item crosses the boundary and comes in through the Execute Sub-workflow Trigger. Inside, check-credit does its job —checks the credit, compares, decides— and its last node produces the result in the contract's shape (the ok envelope with approved and available_credit).
  4. The result crosses back over the boundary. What check-credit's last node produced exits through the boundary and returns to order-triage's Execute Sub-workflow node.
  5. order-triage continues with that result as the item's data. Now the next node can read ok and approved and decide what to do.

The mental image is the waiter at the kitchen window: carries the order slip (steps 1-3), waits at the window (step 2), the kitchen prepares (step 3), the dish comes out through the window (step 4), and the waiter carries it to the table (step 5). The diner —the nodes that follow in order-triage— never saw the kitchen; they just received the finished dish in the shape the menu promised.

A detail that comes out of this model: what order-triage gets back is whatever check-credit's last node produced. If your sub-workflow's last node doesn't have the contract's shape —if it returns internal data, or a half-built object—, that's what the caller receives, contract or not. The output contract isn't fulfilled just by being written in the Sticky Note; it's fulfilled because you make sure the sub-workflow's last node produces exactly that shape. Lesson 8 makes you build that last node carefully precisely for this reason.

What doesn't cross the boundary

Just as important as knowing what crosses the boundary is knowing what does not, because it's a frequent source of confusion and reinforces why the contract has to be explicit.

What crosses the boundary is the item's data: the fields you put in the Execute Sub-workflow node's inputs go inward, and the fields the sub-workflow's last node produces come back outward. That's it. The boundary is narrow on purpose, like the kitchen window: only the order slip and the dish pass through.

What does not cross is the rest of order-triage's context. check-credit doesn't see order-triage's other nodes, doesn't have access to data that stayed in the caller's earlier nodes, and doesn't share its execution memory. It's a separate building: it only knows what you explicitly handed it at reception. If check-credit needs a piece of data to work, that data has to be in the input contract —it can't "go fetch it" from the workflow that called it—.

This has two practical consequences worth burning into memory:

If a field isn't in the input contract, the sub-workflow doesn't have it. There's no way for check-credit to "reach" a field from order-triage you didn't pass it. That's why lesson 3's schema design matters so much: if you forgot to include a field the sub-workflow needs, there's no back door to recover it through; you have to add it to the contract. The boundary forces you to be explicit, and that obligation is a virtue —it makes the sub-workflow's dependencies visible in its contract, not hidden in assumptions—.

State isn't shared between the two sides. Each workflow has its own execution context. What a sub-workflow "remembers" between calls, or state that survives an execution, is a separate topic —Module 4's, the system's data model—. Here, at the boundary, every call is a clean conversation: what you pass it goes in, what it produces comes out, and no shared memory remains just from having called it. That cleanliness is what makes a sub-workflow predictable: its result depends only on what enters through the contract, not on invisible state dragged from previous calls.

Worked example: connecting order-triage to check-credit

Let's build the full call, step by step. If you have an n8n instance handy, follow along; if not, read it and do it later. Button and option names can vary by version —check them on your panel—.

Step 1 — The sub-workflow with its reception. Create a new workflow called check-credit. Its first node is an Execute Sub-workflow Trigger (look for it as "Execute Sub-workflow Trigger" or "When Executed by Another Workflow"). Open it, set "Input data mode" to "Define using fields below," and declare your schema's four fields:

customer_id : string
order_id    : string
amount      : number
currency    : string

What to expect: the node shows those four fields as the sub-workflow's expected input. It doesn't do anything with them yet —that comes later—, but it already declared the door.

Step 2 — A minimal body for the sub-workflow. For now, to test the boundary, connect an Edit Fields (Set) node after the trigger that builds a success response in the contract's shape. Set it in JSON mode to something like this:

{
  "ok": true,
  "customer_id": "{{ $json.customer_id }}",
  "approved": true,
  "available_credit": 5000
}

It isn't the real credit logic yet —it's fixed—, but it has the contract's shape, which is what we need to test the call. Save the workflow.

What to expect: check-credit is now a complete sub-workflow: it receives through the door, and its last node produces a response in the ok envelope's shape.

Step 3 — The caller. Go to order-triage (or create a test workflow with a Manual Trigger and an Edit Fields that builds a Cumbre order with customer_id, order_id, and amount). At the point where you want to check the credit, add an Execute Sub-workflow node. In Source choose your list of workflows, and in Workflow select check-credit.

What to expect: when you select check-credit, n8n shows you the fields you declared in the trigger —customer_id, order_id, amount, currency— ready to fill in. There's the contract helping you: you don't have to remember what check-credit expects, the node tells you.

Step 4 — Fill in the inputs and choose the mode. Fill each field with the corresponding value from the order (customer_id with {{ $json.customer_id }}, and so on). Set Mode to "Run once for each item," because the contract is per order. Verify that "Wait for Sub-Workflow Completion" is active. Run it.

What to expect: order-triage's Execute Sub-workflow node shows, in its output, the response check-credit produced: { ok: true, customer_id: "...", approved: true, available_credit: 5000 }. You just crossed the boundary in both directions: you sent an input in the contract's shape and received an output in the contract's shape. order-triage's next node can now read ok and approved.

If instead you see an error or an empty output, there are two typical causes: either the earlier node didn't run and there's no order to send (check that the Edit Fields ran), or some required field was left unfilled in the Execute Sub-workflow node's inputs. The boundary is demanding on purpose.

Common mistakes

Confusing the caller node with the trigger node (practical). What happens: someone puts an Execute Sub-workflow Trigger in the workflow that calls instead of the one that responds, or the other way around, and the call doesn't connect —the sub-workflow "receives nothing" or the caller "can't find who to call"—. Why it happens: both nodes have "Sub-workflow" in the name and it's easy to grab the wrong one from the node panel. How to spot it: remember the reception rule —the Trigger ("When Executed by Another Workflow") goes as the responder's first node; the Execute Sub-workflow goes in the middle of the caller—. If your sub-workflow doesn't start with the Trigger, or your caller doesn't have the Execute Sub-workflow node, that's the mix-up. How to fix it: the responder starts with the Trigger; the caller has the Execute Sub-workflow at the point of the call. One is the reception, the other is the visitor.

Choosing the wrong mode and receiving an unexpected input shape (practical). What happens: check-credit's contract is written for one order, but the caller invokes it in "Run once with all items" mode, so the sub-workflow receives a batch of several orders at once instead of one; the internal logic, written for a single order, processes it wrong or only handles the first one. Why it happens: the default mode and the mental one don't always match, and the effect doesn't jump out until two or more items arrive together. How to spot it: ask yourself "is this sub-workflow's contract written for one item or for a batch?" and compare it against the mode chosen in the Execute Sub-workflow node. If the contract is per-item but the mode is "all items," there's a mismatch. How to fix it: for a sub-workflow with a per-item contract —like check-credit—, use "Run once for each item," so it runs once per order with the input its contract expects. Align the mode with the shape the contract was designed for.

Leaving the sub-workflow on "Accept all data" and believing it has a contract (conceptual). What happens: someone creates check-credit, leaves the Input data mode on "Accept all data" because "that way it accepts whatever's sent," and later is surprised the caller node shows no guiding fields and nothing verifies what comes in. Why it happens: "accept everything" sounds flexible and convenient, and hides that the door making the contract governable was given up. How to spot it: open your contract-bearing sub-workflow's Execute Sub-workflow Trigger; if it's on "Accept all data" and lists no fields, it isn't declaring its schema. How to fix it: for every sub-workflow with a contract, use "Define using fields below" (or "Define using JSON example") and declare the fields. "Accept all data" is for generic utilities that truly process any item, not for a sub-workflow that promises a specific shape.

Exercises

Exercise 1 — Locate each node. For the call between order-triage and check-credit, say which workflow each of these nodes goes in and at what position (first, in the middle), and what role it plays in the reception analogy:

(a) Execute Sub-workflow Trigger. (b) Execute Sub-workflow.

See solution

(a) Execute Sub-workflow Trigger — goes in check-credit (the responder), as its first node. It's the building's reception seen from inside: where the visit arrives and where what's needed to be let through gets declared. On the canvas it shows up as "When Executed by Another Workflow."

(b) Execute Sub-workflow — goes in order-triage (the caller), in the middle, at the point where the credit needs to be checked. It's the visitor showing up at the other building's reception: choosing who to call and filling out the form with their data.

Why this works: confusing these two nodes is one of the most common mistakes, and the reception rule resolves it in one shot: the Trigger is the one that receives (first, in the responder); the Execute Sub-workflow is the one that goes to visit (in the middle, in the caller). If you're clear on the arrow's direction —who calls whom—, you're clear on where each node goes.

Exercise 2 — Choose the Input data mode. For each of these sub-workflows, decide which input data mode you'd use in its Execute Sub-workflow Trigger ("Define using fields below," "Define using JSON example," or "Accept all data") and why:

(a) check-credit, with its four-field schema already designed. (b) A log-anything sub-workflow that receives any item and saves it as-is to a log, regardless of its shape. (c) A new sub-workflow for which you already have a representative example JSON object but haven't written the field list.

See solution

(a) "Define using fields below." You have the explicit schema with four fields, types, and requiredness; transcribing it field by field is the clearest option and gets callers the guide. It's the natural mode for a sub-workflow with a serious contract.

(b) "Accept all data." It's the legitimate case for this mode: log-anything truly has no shape contract —its job is to accept any item—, so declaring fields wouldn't make sense. Here "accept everything" isn't giving up a contract; it's that this sub-workflow's contract is, literally, "I accept anything."

(c) "Define using JSON example." You already have the representative example; pasting it and letting n8n infer the schema is faster than transcribing fields by hand, and it makes use of work you already did. Afterward you can check that the types it inferred match your intent.

Why this works: the mode isn't chosen out of habit, it's chosen based on what the sub-workflow promises. An explicit contract calls for "fields below"; an already-written example calls for "JSON example"; the genuine absence of a shape contract calls for "accept all data." Confusing the third case (genuinely generic utility) with a sub-workflow that does have a contract but that it's lazy to declare is the previous section's mistake.

Exercise 3 — Trace the execution. order-triage receives two orders in a single trigger and calls check-credit in "Run once for each item" mode, with "Wait for Sub-Workflow Completion" active. Describe, step by step, what happens: how many times check-credit runs, when order-triage pauses and continues, and what it gets back.

See solution

Since the mode is "Run once for each item" and two orders arrived, check-credit runs twice, once per order:

  1. order-triage reaches the Execute Sub-workflow node with the two orders.
  2. For the first order: order-triage pauses, the order crosses the boundary, check-credit runs with that single order, produces its response ({ ok: true, approved: ..., available_credit: ... }), and that response comes back.
  3. For the second order: the same repeats —check-credit runs again, now with the second order, and returns its own response—.
  4. order-triage continues with two results, one per order, and its next node processes each.

Since "Wait for Sub-Workflow Completion" is active, order-triage doesn't move on until it has each call's result. If the mode had been "Run once with all items," check-credit would have run a single time receiving both orders together —and, since its contract is written for a single order, it probably would have processed them wrong—.

Why this works: this exercise combines the mode with the execution model. "Run once for each item" turns N items into N calls, each respecting check-credit's per-order contract; and "Wait for Completion" guarantees each result is ready before continuing. Understanding this mechanics is what lets you predict how many times your sub-workflow runs and with what input —which, when the sub-workflow has an effect like issue-refund, is literally the difference between one refund and several—.

Summary and next step

In this lesson you put the contract to work. You saw the Execute Sub-workflow node's boundary as the single door with a doorman that makes a sub-workflow governable: a single entry point where you declare the schema, validate, and know which version you're serving. You told apart the boundary's two pieces —the Execute Sub-workflow Trigger (labeled "When Executed by Another Workflow"), the responder's first node, the reception; and the Execute Sub-workflow node, in the caller, the visitor—. You declared check-credit's schema in the trigger by choosing "Define using fields below," and saw that schema automatically show up as a guide in the caller's node. On the caller's side, you understood the key parameters: Source, Workflow, the inputs, Mode ("Run once for each item" for a per-order contract), and "Wait for Sub-Workflow Completion." And you traced the execution model —pause, run the sub-workflow, cross the result back, continue—, noticing that what the caller receives is whatever the sub-workflow's last node produces, whether or not it has the contract's shape. And you remembered that in n8n 2.0 this node is called "Execute Sub-workflow," formerly "Execute Workflow," with exact labels always to be verified on your own panel.

Before moving on to lesson 5 you should be able to: build a call between two workflows, putting each node on the correct side; declare the schema in the trigger with the right mode; and explain the pause-and-continue sequence when one workflow calls another.

What you have so far is the door and the doorman standing in it —but the doorman doesn't yet check anything in depth—. Declaring the fields in the trigger tells n8n the names and types, but it doesn't verify business rules: it doesn't prevent a negative amount, or a customer_id that doesn't exist, or malformed data that slipped through. Lesson 5 gives the doorman real work: validating the input at the boundary, rejecting whatever doesn't comply with the contract with a clear message, and making sure bad data never reaches the effect —which, in issue-refund's case, would be a wrong refund that doesn't undo.

Resources