Module 3: Contracts Between Workflows

1. Introduction: the promise between workflows

Description

By the end of this lesson you'll be able to explain why a system with more than one workflow is, at bottom, a system of promises between workflows; you're going to know intuitively what a workflow contract is and why it exists even if nobody wrote it down; and you're going to have the full map of this module's eight lessons. You're also going to reunite with Cumbre, the distributor that accompanies this whole guide, and with its order-triage workflow, which in this module stops working alone and starts calling other workflows.

This matters for a very concrete reason: in the previous two modules your concern was a single workflow firing twice. You learned to see the duplicate problem and you learned to make an effect idempotent so repeating it wouldn't duplicate. But as soon as your system grows past one workflow, a new problem shows up that idempotency doesn't solve: workflows start calling each other, and every call is a promise. One passes data, the other receives it and returns a result. That promise —which fields go, with what types, which are required, and what shape the response has— is a contract. And like every contract nobody put in writing, it breaks at the worst moment and silently: someone changes a field in a workflow, and another workflow that depended on that field starts failing with nothing screaming about it.

Connection to the module: this lesson doesn't teach you to write or validate a contract yet. It's the map. Here you define the problem —why a multi-workflow system is a system of contracts—, you meet the tool that connects them —the Execute Sub-workflow node— and you pick the case study back up with its first sub-workflow, check-credit. Lesson 2 precisely defines what a workflow contract is. Lessons 3 through 6 give you the pieces: designing the schema, crossing the Execute Sub-workflow boundary, validating at that boundary, and versioning without breaking. Lesson 7 takes the same concept into MCP and agent tool territory. And lesson 8 closes by building a validated sub-workflow with a contract end to end. A scope note from the start: here you're not going to coordinate many workflows that depend on each other —that's Module 5— nor decide where the system's state lives —that's Module 4—. This module is about the promise between two workflows: how it's defined, validated, and versioned.

From a solitary workflow to a system that converses

Think of a restaurant. When you order a dish, you don't walk into the kitchen, you don't check how the fridge is organized, and you don't explain to the cook which pan to fry it in. You look at the menu, say "one skirt steak, medium," and trust that something recognizable is going to arrive: a skirt steak, cooked medium, on a plate. The menu is a promise. On the diner's side it says what you can order and with what words; on the kitchen's side it says what they need to know how to prepare. As long as that promise is kept, the diner and the kitchen can each change on their own: the cook can start using a new knife, and the diner isn't affected, because what they were promised —the steak— didn't change.

Now take the menu away from the restaurant. The diner has to shout toward the kitchen describing what they want with whatever words come to mind, and the kitchen has to guess. One day the diner says "red meat" and gets a steak; another day they say the same thing and get a stew, because the cook changed and understood something different. Without a menu, every order is a fragile negotiation that depends on both sides happening to understand the same thing.

An automation system with several workflows is exactly that restaurant. Every time one workflow calls another, there's a diner (the caller) and a kitchen (the responder). The menu is the contract: what data gets passed and what result gets returned. And here's what matters, what makes this module exist: that contract already exists from day one, whether you wrote it or not. The moment order-triage passes a customer_id and an amount to check-credit, and expects back an approved, there's a contract. The only question is whether it's a contract you can see, review, and protect —a menu hanging on the wall— or an invisible contract that lives only in your memory and in the way you happened to wire the nodes today.

The invisible contract works perfectly as long as nobody touches anything. The problem is that automation systems live to be touched: you add a field, you change a type, you rename something to make it "clearer." And every one of those changes, on a contract nobody wrote, is a blind bet on who else depended on what you just changed.

Worked example: the silent break

Let's see the problem in its purest form, without teaching how to fix it yet. This is the scenario that repeats in every company that grows from one workflow to several.

Cumbre has its order-triage workflow: a Webhook receives an order, an AI Agent classifies it, and an HTTP Request registers it in the CRM. Up through the previous module, order-triage did everything alone. But classifying an order includes a heavy business decision: does the customer have enough credit for this order? That logic —querying the customer's balance, subtracting pending orders, comparing against the total— is complex, needed in more than one place, and worth being able to test in isolation. So Cumbre's team pulled it out into its own sub-workflow: check-credit.

Now order-triage calls check-credit and passes it this:

{
  "customer_id": "CUST-118",
  "order_id": "ORD-2041",
  "amount": 1842.50
}

And expects this back:

{
  "customer_id": "CUST-118",
  "approved": true,
  "available_credit": 5157.50
}

order-triage reads the approved field. If it's true, it continues and registers the order; if false, it flags it for manual review. Everything works. Nobody wrote the contract down anywhere, but it's there, alive, in the way the two workflows pass data to each other.

Three weeks later, someone else on the team improves check-credit. They feel approved is a poor name —"approved for what?"— and rename it to credit_approved, which sounds clearer. They save, publish, and in check-credit's isolated tests everything looks fine: the workflow runs, returns credit_approved: true, perfect.

What to expect. The next day, order-triage starts sending every order to manual review. All of them. The good ones and the bad ones. And it throws no error: no red line, no failed execution, no alert. order-triage simply reads approved, finds undefined because the field is now called credit_approved, and undefined isn't true, so it sends everything to review. From the outside, the system "works": the workflows run, there are no exceptions. It's just that the business ground to a halt, and nobody's going to notice until a customer calls asking why their order has been stuck for two days.

Notice what happened, because it's the heart of the whole module. Nobody did anything "wrong" in the technical sense. The person who renamed the field improved check-credit's clarity. The problem is that field was part of a promise with order-triage, and breaking a promise nobody wrote down doesn't produce a loud error —it produces a system that silently does the wrong thing, which is much worse—. A workflow that crashes warns you. A workflow that broke a contract lets you believe everything is fine.

This entire module is about turning that invisible contract into a visible one: written, validated at the boundary, and versioned so renaming a field is a conscious, safe decision instead of a ticking time bomb.

Why idempotency isn't enough here

You might be thinking: "in module 2 I learned to make things robust, isn't this the same thing?" It's worth separating them carefully, because they're two different problems that get confused all the time.

Idempotency —module 2's topic— answers the question "what happens if this operation runs twice?". It's a defense against repetition: a webhook firing double, a retry calling the same API again, an effect applying more than once. Idempotency lives inside an effect and protects it from itself.

The contract —this module's topic— answers a completely different question: "what happens when two workflows have to understand each other and one of them changes?". It's a defense against misunderstanding between parties. It has nothing to do with how many times something runs; it has to do with whether the caller and the responder still speak the same language after one of them evolved.

Think of it this way: idempotency protects a workflow from repeating itself; the contract protects two workflows from losing mutual understanding. A system can be perfectly idempotent —every effect applies exactly once, no matter how many times you trigger it— and still break completely because someone renamed a field in a sub-workflow. Both problems get solved with different tools, and both need solving. This module is the second one.

There's a point where they touch, and you're going to see it in lesson 5: when you validate an input at the boundary, you're preventing malformed data from reaching an effect. If that effect also moves money —like issue-refund, the sub-workflow that issues refunds—, the combination of a validated contract and an idempotent effect is what separates a system you can leave running from one you have to pray over. But that's the union of the two modules; first you need the contract.

This module's case study: Cumbre, order-triage, and its sub-workflows

As throughout the guide, we work with Cumbre, the Latin American wholesale distributor of coffee and tea that sells to about 400 cafés. If you're coming from the previous modules, you already know its star workflow, order-triage, and its way of receiving orders through three channels of varying data quality.

What's new in this module is that order-triage stops being an island. From here on, when an order arrives, order-triage delegates heavy decisions to specialized sub-workflows:

Sub-workflowWhat it doesWhat it receivesWhat it returns
check-creditChecks whether the customer has enough credit for the ordercustomer_id, order_id, amountapproved, available_credit
issue-refundIssues a refund for an orderorder_id, amount, reasonrefund_id, status

check-credit is our main sub-workflow in this module: we're going to design it, document it, validate it, and version it across the eight lessons, until we build it entirely in the project. issue-refund shows up when we need to talk about an effect that moves money and therefore demands an especially careful contract —because a wrong refund doesn't undo with a button—.

Identifiers are in English, as throughout the guide and the entire real market: order-triage, check-credit, issue-refund, customer_id, order_id, amount, approved. The prose you're reading is in English here; the code comments too. It's the mix you're going to find on any team in the region.

An honest note, the usual one: Cumbre's data is made up. The credit limit, the amounts, the customer names are reasonable hypotheses for practice, not market figures. What transfers from here to your real work isn't the numbers, it's the way of thinking about the promise between two workflows.

What a call between workflows looks like, under the hood

Before closing the introduction, it's worth looking at the mechanics in slow motion, even though we'll take them apart in full by lesson 4. You need a mental image of what's physically happening when one workflow calls another, because the six lessons that follow rest on that image.

A call between two workflows in n8n has three pieces, and it's worth naming them now:

The caller. It's order-triage. Somewhere on its canvas it has a node —the Execute Sub-workflow node— that says, in essence, "stop here, run that other workflow with this data, and continue when it comes back with its result." That node is the waiter who carries your order to the kitchen and waits at the window.

The boundary. It's the exact point where order-triage ends and check-credit begins. Everything that crosses that boundary going in is the sub-workflow's input; everything that crosses back going out is its output. The boundary is the kitchen window: the only thing that passes through it is the order slip and the finished dish. The diner doesn't see the kitchen and the kitchen doesn't see the table.

The responder. It's check-credit. Its first node isn't a Webhook or a Schedule; it's a special trigger that exists for one thing only: receiving the call from another workflow. That node is the window seen from inside the kitchen —where the order slip arrives—.

Put in a diagram, the example's call looks like this:

order-triage (the caller)                      check-credit (the responder)
──────────────────────────                     ──────────────────────────────
[Webhook]                                       [trigger that receives the call]
   │                                                    │
[AI Agent]                                        (in here: query credit,
   │                                                compare, decide)
[Execute Sub-workflow] ─── input ──►                    │
   │            ◄────────── output ───────────── [last node returns the result]
[HTTP Request to CRM]

Notice what that diagram makes obvious: the contract lives exactly in the two arrows in the middle. The input arrow is the promise of what order-triage sends check-credit; the output arrow is the promise of what check-credit returns to order-triage. Everything else —how the kitchen is set up inside, which nodes check-credit uses to check the credit— isn't part of the contract and can change freely. The only thing the two parties promised each other is those two arrows.

That's why the worked example's approved rename was so destructive: it didn't touch the kitchen, it touched the arrow. Changing how check-credit calculates the credit internally wouldn't have broken anything; changing the name of a field that travels along the output arrow broke everything. The rule that distills from here, and that you're going to see again and again in this module, is easy to say and easy to forget: you can change the kitchen whenever you want; the window is sacred.

With that image —caller, boundary, responder, and the contract living in the two arrows— you have what you need for the rest of the module. Lesson 4 puts the real name on each piece in n8n 2.0's interface; for now, hold on to the shape.

What you'll be able to do by the end of the module

This module's exit skill is bounded and concrete. By the end of lesson 8 you're going to be able to:

  • Define an input and output contract for a sub-workflow: which fields go in, with what types, which are required and optional, and what shape the response has both on success and on error.
  • Validate that contract at the boundary: make the sub-workflow reject an input that doesn't comply, with a clear message, before running any effect.
  • Version the contract, telling apart a compatible change —one that doesn't break existing callers— from a breaking one, and knowing how to run two versions side by side while you migrate the callers.
  • Write the contract for a workflow exposed as a tool for an AI Agent or an MCP client, so the agent uses it at the right moment and with the right data.

What you're not going to do in this module, and that's fine: you're not going to coordinate three or more interdependent workflows (Module 5), you're not going to design where the system's state lives or a deduplication ledger (Module 4), and you're not going to build retry and alerting logic (Module 6). Here the focus is the promise between two workflows, made visible.

This module's map

LessonWhat it solves
2Exactly what a workflow contract is: the promise between caller and responder, the analogy with a function signature, and why writing it down prevents silent breaks
3How to design the input and output schema as an object: names, types, required vs. optional, default values, and the response shape on success and on error
4The Execute Sub-workflow node's boundary: where one workflow calls another, what passes, what returns, and why a single entry point is worth it
5How to validate the input at the boundary: rejecting invalid data early, with a useful message, before it reaches the effect
6How to version a contract without breaking callers: compatible vs. breaking changes, parallel versions, and gradual migration
7What the contract looks like when an AI Agent or an MCP client consumes your workflow as a tool: the Description plus the input schema as a stable signature
8Project: build check-credit with a documented contract, boundary validation, and a second compatible version

Notice the order, because it isn't accidental. First what it is (lesson 2), because you can't protect something you can't name. Then how it's designed (lesson 3): the schema on paper, before touching n8n. Then where it lives (lesson 4): the Execute Sub-workflow node's concrete boundary. Only then how it's protected: validating (5) and versioning (6). Lesson 7 extends the same concept into agent territory, and 8 pulls it all together into a deliverable.

What this module deliberately doesn't cover

It's worth saying this early, so you know where to look for what isn't here.

It isn't the module on the system's data model. Where the truth lives —the state that survives across executions, the ledger that remembers what you've already processed— is Module 4. Here a sub-workflow receives data, decides, and returns; it doesn't keep long-term memory.

It isn't the module on dependencies between many workflows. Coordinating three or more workflows, fan-out and fan-in, the outbox pattern, and execution order are Module 5. Here we work the relationship between two: one that calls and one that responds.

It isn't the module on retries or recovery. What happens when a sub-workflow fails halfway, how to retry without duplicating, where failures should alert, and how to reproduce a bug with n8n 2.0's replay is Module 6. Here a contract defines what a valid input is; what to do when something truly falls over comes later.

It isn't the AI agents as a product guide. Lesson 7 touches on how an agent consumes a workflow as a tool, focused on the contract. Building the agent, its tools, its memory, and its behavior is the ecosystem's chatbots and agents guide.

Common mistakes

Believing the contract doesn't exist until you write it (conceptual). What happens: someone connects order-triage to check-credit, sees it works, and concludes that since "no contract was defined," there's none to protect; they start changing fields freely. Why it happens: the word "contract" sounds like a formal document, and since there's no document, it seems there's no obligation. But the contract isn't the document —it's the real dependency between the two workflows, which exists from the first time one passed data to the other—. How to spot it: ask yourself "if I rename this field in the sub-workflow, does some other workflow stop working?" If the answer is "yes" or "I'm not sure," there's a live contract, written or not. How to fix it: treat every field a sub-workflow receives or returns as part of a promise, from day one. Writing it down (lesson 3) doesn't create the obligation; it just makes it visible so you can respect it.

Confusing "no error was thrown" with "it worked" (conceptual). What happens: someone changes a sub-workflow, tests it in isolation, sees it runs with no exceptions, and considers the change good; the caller, meanwhile, ended up silently broken. Why it happens: intuition says a problem shows up as an error, and contract breaks almost never throw one —they produce an undefined, a branch that doesn't get taken, an effect that doesn't get applied—. How to spot it: after touching a sub-workflow, testing the sub-workflow alone isn't enough; you have to run at least one caller end to end and verify the business result is still correct, not just that "it didn't crash." How to fix it: adopt the rule that changing a contract requires testing the callers, not just the one that changed. Lesson 6 formalizes how to find those callers before touching anything.

Thinking module 2's idempotency already solved this (conceptual). What happens: someone who made every effect idempotent assumes their system is already robust, and is surprised when a field rename breaks it entirely. Why it happens: both topics use "reliable system" vocabulary and it's easy to lump them into the same box. But idempotency protects against an operation repeating, not against two workflows misunderstanding each other. How to spot it: if your system broke with nothing running twice —a workflow simply stopped understanding another— it wasn't an idempotency problem. How to fix it: keep the two lenses separate. "What happens if this runs twice?" is idempotency; "what happens if the other workflow changes?" is contract. Both matter and get solved differently.

Exercises

Exercise 1 — Find the invisible contract. Take this lesson's worked example: order-triage passes check-credit an object with customer_id, order_id, and amount, and expects back approved and available_credit. Without looking at lesson 3, write in your own words check-credit's complete contract: which fields go in, which seem required to you, and which fields come out. Then point out which of those fields, if renamed, would break order-triage.

See solution

The contract, informally stated, is something like: "check-credit receives a customer_id (required, to know whose credit to check), an order_id (required, to know which order the query belongs to), and an amount (required, the order total compared against available credit), and returns an approved (true or false) and an available_credit (the credit the customer has left)."

The field whose rename would break order-triage is approved, because it's the one the caller reads to decide whether to continue or send to review. If check-credit stops returning approved with that exact name, order-triage reads undefined, and undefined isn't true, so it sends everything to review —exactly the worked example's silent break—.

Why this works: the exercise forces you to put into words a contract that until now only lived in the node connections. That act —naming the promise— is this whole module's first step. And noticing that a single field (approved) is what holds up the relationship shows you how fragile an unwritten contract is: a well-intentioned rename topples it.

Exercise 2 — Idempotency or contract. For each of these four problems, decide whether it's an idempotency problem (repeating without duplicating) or a contract problem (two workflows losing mutual understanding), and justify in one sentence:

(a) order-triage's webhook fires twice for the same order and two records get created in the CRM. (b) Someone changed amount's type in check-credit from number to text, and now the credit comparison gives absurd results. (c) An issue-refund retry issues a second refund for the same order. (d) check-credit stopped returning available_credit because someone deleted that field from the sub-workflow, and another workflow that displayed it in a report now shows a blank.

See solution

(a) Idempotency. The problem is the same operation ran twice and produced two effects. There's no misunderstanding between workflows; there's an unprotected repetition.

(b) Contract. Nobody ran anything twice. The problem is the sub-workflow changed a field's type (number to text) and the caller no longer understands it the way it used to. It's a break of the promise about what shape the data has.

(c) Idempotency. Same as (a): an operation that moves money repeated and produced two effects. The defense is an idempotency key, not a contract.

(d) Contract. A field that was part of the output promise disappeared, and a consumer that depended on it broke. Nobody repeated anything; someone changed what the sub-workflow promises to return.

Why this works: all four cases sound like "the system failed," but they split cleanly into two families. (a) and (c) are repetitions —the same act happened twice—; (b) and (d) are misunderstandings —the act happened once, but the two parties no longer speak the same language—. Knowing which family a problem belongs to tells you which tool to attack it with, and that diagnosis is half the job.

Exercise 3 — Rebuild the map. Without looking back at the "This module's map" table, write from memory what each of the seven following lessons (2 through 8) solves, one sentence each. Then compare and mark the ones you missed.

See solution

(2) Exactly what a workflow contract is and why writing it down prevents silent breaks. (3) How to design the input and output schema: names, types, required vs. optional, default values, and response shape. (4) The Execute Sub-workflow node's boundary: where one workflow calls another and what crosses that boundary. (5) How to validate the input at the boundary and reject invalid data with a clear message. (6) How to version a contract without breaking callers: compatible vs. breaking. (7) What the contract looks like when an agent or an MCP client consumes the workflow as a tool. (8) The project: build check-credit with a contract, validation, and a second compatible version.

Why this works: if you rebuilt at least five of the seven, you've already internalized the module's progression, which goes from the concept (what is it?) to the design (how is it written?) to the mechanics (where does it live and how is it protected?). The ones people usually miss are 5 and 6, which only become concrete once you see them applied to check-credit.

Summary and next step

In this lesson you saw that as soon as a system has more than one workflow, the workflows call each other, and every call is a promise —a contract— about what data goes in and what result comes back. You saw the restaurant image with and without a menu: as long as the promise is kept, both sides can change internally without affecting each other; with no written promise, every order is a fragile guessing game. You met the silent break —someone renames approved to credit_approved in check-credit, and order-triage starts sending everything to manual review without throwing a single error—, and you understood why that kind of break is worse than a loud crash: a workflow that crashes warns you; one that broke a contract lets you believe everything is fine. You separated the contract problem from the idempotency problem: one protects two workflows from losing mutual understanding, the other protects one workflow from repeating itself. And you got Cumbre back, with order-triage calling check-credit for the first time, the sub-workflow you're going to design, validate, and version throughout the module.

Before moving on to lesson 2 you should be able to: explain in one sentence why a multi-workflow system is a system of contracts; describe in your own words the contract between order-triage and check-credit; and tell apart a contract problem from an idempotency problem.

What you don't have yet is the precise definition. We talked about the contract as "the promise," but a promise you want to protect needs concrete parts: which fields, with what types, which required, what output shape. Lesson 2 gives that promise its exact name and anatomy, with the function-signature analogy, so in lesson 3 you can write it.

Resources