Module 3: Contracts Between Workflows
7. Contracts for MCP and agent tools
Description
By the end of this lesson you'll be able to expose a sub-workflow as a tool an AI Agent or an external MCP client can use, and write its contract so the model uses it at the right moment and with the right data. You're going to understand that for this type of caller the contract no longer looks like fields in an Execute Sub-workflow node, but like a name, a natural-language description, and an input schema, and that a clear contract is literally what makes the agent choose your tool correctly instead of another one. And you're going to see that everything you built in this module —validating at the boundary, versioning carefully, declaring the output shape— still applies, only now the caller is a model.
This matters because your contracts' caller is changing. Up through this lesson, whoever called check-credit was always another workflow: order-triage with its Execute Sub-workflow node, filling in concrete fields. But increasingly, whoever consumes a workflow is an AI Agent —order-triage's own agent— or, with n8n's MCP capabilities, an external client like Claude Desktop, Cursor, or ChatGPT building and firing workflows inside your instance. That caller doesn't fill in fields: it reads a description and decides. If the description is vague, the model guesses —and guessing in a system that checks credit or issues refunds gets expensive—.
Connection to the module: the whole module built the contract between two workflows. This lesson carries it over to a new caller —a model— and shows the contract is the same, expressed differently. It rests directly on the ecosystem's chatbots and agents guide, which teaches the tool contract (Name, Description, parameters with $fromAI()) and the trust boundary (human review for sensitive actions): what you see here is that same concept, applied to a full sub-workflow exposed as a single tool, and tied to the validation (lesson 5) and versioning (lesson 6) you've already practiced. Lesson 8 closes the module by building the validated sub-workflow; this lesson gives it the face it shows when whoever calls it is an agent.
A diner who only has the menu
Let's go back to the restaurant one last time, because the analogy still has something to teach. Up to now, the diner ordering from your menu was another workflow: it knew exactly which dishes existed because the Execute Sub-workflow node showed them as fields ready to fill in. It was a diner who already knew the menu by heart.
The new diner is different. It's someone who arrived at the restaurant for the first time, can't call the waiter over to ask anything, and has to decide what to order by reading only the menu. If the menu says "Dish of the day," that diner has no idea what's going to arrive, and either orders fearfully or orders something else. If the menu says "Charcoal-grilled skirt steak, choice of doneness, with a nopal cactus side," the diner knows exactly what they're ordering and when to order it. For this diner, the menu's text is all they have. There's no possible conversation; there's only what's written.
An AI Agent facing your tools is exactly that diner. It doesn't read check-credit's internal nodes, it can't ask you what you expected, it doesn't see the contract's Sticky Note. All it has to decide whether to use your tool, when to use it, and with what data to call it is text: the tool's name, its description, and each parameter's description. That text is the whole contract, and like the diner who only has the menu, if the text is vague, the model guesses. The chatbots guide says it bluntly and it's worth repeating here: a tool's contract is its text, and guessing in a system that touches real data gets expensive.
The good news is you already know how to write contracts. Everything you learned —specific names instead of vague ones, a precise input schema, a clear output shape— is exactly what makes a tool's contract good. The difference is that now one piece of the contract, the description, carries new weight: it's what the model reads to decide. In a contract between workflows, the description was documentation for the team; in a contract for an agent, the description is functional —the model uses it to act—.
The two ways a workflow becomes a tool
n8n gives you two paths for a workflow to be consumable as a tool, and it's worth telling them apart because the caller is different in each. The exact node names can vary between versions —check them on your panel—, but each path's concept is stable.
Path 1 — A sub-workflow as an AI Agent's tool, inside n8n. When you have an AI Agent in a workflow (like the one classifying orders in order-triage), you can connect it a complete sub-workflow as one of its tools, using a node that exposes that sub-workflow to the agent —on your version it might show up as "Call n8n Workflow Tool" or "Custom n8n Workflow Tool"; check the label—. The agent sees that sub-workflow as a single tool with a name and a description, with no knowledge that underneath it's several nodes. It's the natural evolution of the tool contract the chatbots guide teaches: there a tool was a node (a Gmail, an HTTP Request); here a tool is a whole sub-workflow with its contract. check-credit exposed this way becomes a tool order-triage's agent can invoke when it needs to decide about credit.
Path 2 — Your instance as an MCP server for external clients. MCP —Model Context Protocol— is a standard that lets external AI tools connect to services and use their capabilities. n8n can act as an MCP server via the MCP Server Trigger node: that node turns your instance into an entry point external MCP clients —Claude Desktop, Cursor, ChatGPT— can connect to in order to use your workflows as tools. The workflows you want to expose get hooked up to the MCP Server Trigger (with the same kind of tool node from path 1), and the trigger publishes a URL —there's a test one and a production one— protected by authentication (Bearer or Header). That way, check-credit can become a tool an AI assistant outside n8n invokes, within the limits you define.
Both paths share what matters for this lesson: in both, the workflow presents itself as a tool with name + description + input schema, and in both the caller is a model that decides by reading that text. What changes is where the model lives —inside your workflow (path 1) or in an external application (path 2)—. The contract you write is the same.
A tool's contract: name, description, parameters
Let's gather the three pieces of a tool's contract as the chatbots guide teaches them, now applied to a sub-workflow exposed as a tool. It's the same skeleton as always —what goes in, in what shape— but with the description carrying the new weight of being read by the model.
The name (Name). What the tool is called. The agent uses it to refer to it and as the first clue to what it does. A name like check_credit says what it's about; one like subworkflow_1 says nothing and forces the model to rely only on the description. The specific names you practiced in lesson 3 hold just as true here: check_credit, not process.
The description (Description). The text telling the model what the tool does and —as important as that— when NOT to use it. It's the piece that most shapes the agent's behavior, because it decides at what moment the tool enters consideration or not. A good tool description says three things: what it does, when to use it, and when not to. For check-credit: "Checks whether a customer has enough credit for an order. Use it before confirming an order on credit. Do not use it for cash orders or for issuing refunds." That last sentence —the "do not use it for…"— is what keeps the agent from confusing this tool with a similar one, the overlapping-descriptions problem the chatbots guide covers in detail.
The parameters (the input schema). Every piece of data the tool needs. When the sub-workflow is connected to a Tools Agent, the parameters get filled in with the $fromAI(key, description, type, defaultValue) function, where key is the data's identifier (1 to 64 characters), description tells the model what to look for for that piece of data, type is string, number, boolean, or json, and defaultValue is optional. Notice this is your lesson-3 input schema, with one more layer: every field doesn't just have a name and a type, but a description for the model of where to get its value from. $fromAI("customer_id", "The ID of the customer placing the order, taken from the order record, never invented", "string") tells the model exactly what to put there and where from —the order record— and what not to do —invent it—.
check-credit's complete contract as a tool:
TOOL — check-credit
Name: check_credit
Description:
Checks whether a customer has enough credit for an order.
Use it before confirming an order on credit, when you need to know
whether the order amount fits within the customer available credit.
Do NOT use it for cash orders, for issuing refunds
(use issue_refund for that), or for changing the credit limit.
Parameters (via $fromAI):
customer_id : string — "Customer ID from the order record, never invented"
order_id : string — "ID of the order being evaluated"
amount : number — "Order total to compare against the credit. Must come
from the order, not be estimated or rounded."
Worked example: the description decides which tool the agent uses
Let's see why the contract's text is functional and not decorative. order-triage's agent has two tools connected, exposed as sub-workflows: check-credit (checks credit, read-only) and issue-refund (issues a refund, moves money). A customer writes in the chat: "I want to place a 2000-peso order, do I have enough credit?".
With poorly written contracts. Say both tools have weak descriptions: check-credit says "Handles customer credit" and issue-refund says "Processes customer money operations." Both descriptions overlap —both sound like "something with customer money"—, so the model, comparing the request against both, splits its decision between them. It might choose check-credit, which is correct; but it might also misread "money operations" and consider issue-refund. In the worst case, faced with a request that only asked to check, the agent ends up considering a tool that moves money.
With well-written contracts. Now check-credit says "Checks whether a customer has enough credit. Use it to answer whether a customer has enough credit. Does NOT issue refunds or move money." and issue-refund says "Issues a refund on an already-paid order. Do NOT use it to check credit or for new orders." The descriptions are now mutually exclusive: each explicitly says what it does and doesn't, and names the other to draw the boundary.
What to expect. With well-written contracts, faced with "do I have enough credit?", the agent reads that check-credit is exactly for that and that issue-refund explicitly isn't for checking; it chooses check-credit, calls it with the correct customer_id and amount, and answers with the result. The tool that moves money never even entered consideration, because its own description ruled it out for this request. With poorly written contracts, the same customer and the same model can produce a wrong choice —not because the model is worse, but because the contract's text didn't give it enough to decide well—. The difference between a reliable agent and an unpredictable one, here, didn't come from the model: it came from the contract.
What doesn't change: the boundary is still the boundary
It's tempting to think exposing a workflow to an agent is a separate world, with new rules. It isn't. Everything you built in this module still applies, and in some cases matters more, not less. It's worth going over it, because it's what keeps you from the mistake of believing "connecting it to an agent" replaces contract engineering.
Boundary validation is still mandatory (lesson 5), and more so here than ever. A workflow calling check-credit sends you fields a developer configured; a model calling check-credit sends you fields it inferred from a conversation's context. The model can be wrong: it can pass an amount it misinterpreted, a customer_id it confused, a number it made up. That's why lesson 5's validation —rejecting at the door what doesn't comply with the contract— isn't just still valid when the caller is an agent, it's even more important: it's the net catching the model's mistakes before they reach the effect. Each parameter's description reduces those mistakes (it tells the model what to put there), but doesn't eliminate them; boundary validation is what genuinely stops them.
The trust boundary applies to effects (chatbots guide + Module 2). check-credit only reads, so letting the agent call it freely is safe. But issue-refund moves money, and there a clear contract isn't enough. The chatbots guide teaches the human review pattern: an irreversible or financially impactful action gets connected behind a human approval step, so the real effect doesn't happen until a person approves it —even if the agent decides to call it—. And Module 2's idempotency guarantees that, even approved, the effect doesn't apply twice if the call repeats. A sub-workflow that moves money, exposed as an agent tool, needs all three layers: a clear contract (so the agent uses it well), boundary validation (to catch data the model got wrong), and a trust boundary plus idempotency (so the effect doesn't happen wrong or twice). The contract tells the agent what to do; the other layers guarantee what happens even if the agent gets it wrong.
Versioning still applies (lesson 6). Changing a tool's description or schema changes how the agent uses it. Renaming a parameter, changing its type, or rewriting the description so the model interprets it differently is a change that can alter the agent's behavior —the equivalent, in this world, of breaking a caller—. The same compatible-vs-breaking principles apply: adding an optional parameter with a default is safe; changing an existing one's type or rewriting the description so the agent stops choosing the tool when it should have isn't. A contract change is a contract change, whether the other side has a workflow or a model.
Exposing with limits: what you put behind an MCP URL
Path 2 —your instance as an MCP server— adds a consideration path 1 doesn't have, and it's worth handling carefully: when you expose workflows via MCP, on the other side there's a client external to your instance. In path 1, the agent lives inside your own workflow, under your control. In path 2, whoever calls can be Claude Desktop on someone else's machine, or an assistant you didn't configure. That changes the security question: it's no longer just "does the agent choose well?", but "what am I giving permission to do to something that lives outside my instance?"
From there come three limits worth setting when exposing via MCP, and none replaces the previous one:
Authentication at the server's door. The MCP Server Trigger publishes a URL protected by authentication —Bearer or Header—. That's the first limit: only whoever has the credential can connect. Treating that credential as a real secret —not pasting it into a chat, not leaving it in a repository— is the floor, because whoever has it can invoke everything you exposed. The details of managing secrets and URLs in production belong to the operations guide; here the point is that the MCP URL isn't public or harmless: it's an entry point into your instance.
Carefully choosing which workflows you hook to the server. Not every workflow should be an MCP tool. A good candidate is a read with a clear contract —check-credit, get-order-status—: useful, bounded, no irreversible effect. A bad candidate, or at least one demanding maximum care, is a workflow that moves money or deletes data. Lesson 5's rule and the chatbots guide's table don't disappear because the caller is external; they get stricter. If you expose issue-refund via MCP, it has to carry its boundary validation, its trust boundary (human review), and its idempotency —all three—, because now whoever triggers it isn't even inside your house.
Each tool's contract is your control surface. What an external MCP client can do with your instance is defined, exactly, by the contracts of the tools you exposed: their names, their descriptions, their input schemas, and —above all— their effect limits. A loose contract exposed via MCP isn't just a risk of the agent choosing wrong; it's a door wider than you wanted into your instance. That's why everything in this module converges here: a well-designed, validated contract with clear effect limits isn't just good engineering —when you expose it via MCP, it's your security control—.
The practical conclusion: exposing via MCP is powerful and worth doing with this whole module's same contract discipline, taken up a notch. Start by exposing reads with clear contracts; treat any effect with the three layers of protection; and remember every tool you hook to the server is a capability you're giving something living outside your instance. Fully operating an MCP server in production —monitoring, credential rotation, scaling— is the operations guide's topic; correctly designing its contracts is this module's.
Common mistakes
Believing connecting the workflow to an agent replaces validation (conceptual). What happens: someone exposes check-credit as a tool with a carefully written description, and since the agent "understands" what to send, removes or never builds boundary validation; in production, the model misreads an amount and the sub-workflow processes it as if correct. Why it happens: a good parameter description reduces the model's errors so much it gives the impression validation is extra. How to spot it: ask yourself "if the model sends the wrong data, does anything reject it?"; if the only defense is the parameter description, there's no real defense. How to fix it: the description for the model and boundary validation are different, complementary layers —one reduces errors, the other stops them—. A workflow exposed to an agent needs both, just like one called by another workflow, and more so because the model infers its inputs instead of receiving them configured.
Overlapping descriptions between two tools (practical). What happens: check-credit and issue-refund have similar descriptions —both talk about "customer money"— and the agent starts calling the wrong one, or alternates between them on similar requests. Why it happens: the model chooses which tool to use by comparing the request against each description's text; if two descriptions overlap, the choice probability splits instead of resolving. How to spot it: review the agent's logs looking for cases where the invoked tool doesn't match what the user asked for. How to fix it: make descriptions explicitly mutually exclusive —each says what it does, what it doesn't, and names the other to draw the boundary—, instead of leaving the difference implicit in the name. It's the same advice from the chatbots guide, applied to sub-workflows exposed as tools.
Exposing a money-moving sub-workflow with no trust boundary (conceptual). What happens: someone exposes issue-refund as an agent tool with a clear contract, and leaves it connected directly to the agent with no approval barrier at all; the agent, faced with a persistent conversation or an unusual phrasing, ends up triggering a refund it shouldn't have. Why it happens: the clear contract and validation make everything work fine in normal tests, and it feels like the system is already safe. How to spot it: for every tool exposed to the agent, ask yourself "what happens, worst case, if the agent calls it when it shouldn't have?"; if the answer involves money, deleted data, or a commitment to a customer, the trust boundary is missing. How to fix it: every tool with an irreversible or financial effect goes behind the chatbots guide's human review pattern, not just behind a good description. The description tells the agent what to do; human review guarantees what happens even if the agent gets it wrong. And Module 2's idempotency ensures that, even approved, the effect doesn't get duplicated.
Exercises
Exercise 1 — Rewrite a tool's contract. A sub-workflow exposed to the agent has this contract: Name apply, Description "Applies things to the order", and a parameter {{ $fromAI("v") }}. The sub-workflow actually applies a discount to an order, and should only be used for already-authorized discounts, never to change the base price or the quantity. Rewrite Name, Description, and the parameter following the lesson's pattern.
See solution
Name: apply_authorized_discount
Description:
Applies an already-authorized discount to an existing order.
Use it only when a confirmed discount authorization exists
for that order. Do NOT use it to change a product base price,
an order quantity, or to authorize the discount
(authorization is a prior step, this tool does not do it).
Parameters:
order_id : {{ $fromAI("order_id", "ID of the order the discount applies to,
taken from the order record", "string") }}
discount_pct : {{ $fromAI("discount_pct", "Discount percentage authorized
for this order. Must come from the authorization, never invented.
Between 0 and 100.", "number") }}
Why this works: the Name went from apply (says nothing) to apply_authorized_discount (says exactly what it does). The Description says what it does, when to use it (already-authorized discount), and —most important— what it does NOT do (base price, quantity, authorizing). And the v parameter, which gave the model no clue, became two parameters with a name and description anchoring each value to its source ("from the authorization, never invented"). A contract like this leaves little room for the agent to misuse it or pass invented data.
Exercise 2 — Straight to the agent or behind human review? For each of these sub-workflows exposed as order-triage's agent's tools, decide whether it can connect straight to the agent or needs to go behind a human review step, and justify with the reversibility and impact criterion:
(a) check-credit — checks credit, read-only.
(b) issue-refund — issues a refund, moves money.
(c) get-order-status — returns an order's status, read-only.
(d) cancel-order — cancels an order with the carrier, irreversible once processed.
See solution
(a) check-credit — straight to the agent. It only reads; calling it extra times changes nothing in the world. There's no effect to protect.
(b) issue-refund — behind human review. It moves money and is hard to undo. Even if the agent calls it by mistake, the real refund shouldn't happen until a person approves it. On top of that, its call has to be idempotent (Module 2).
(c) get-order-status — straight to the agent. It's a read; it produces no effect. Safe to call freely.
(d) cancel-order — behind human review. It's irreversible once the cancellation enters processing with the carrier. The cost of a wrong cancellation is high, so it needs human approval before executing.
Why this works: the criterion isn't how complex the tool is, but how expensive it is to undo the error if the agent gets it wrong. Reads (a, c) have no error cost because they change nothing; irreversible or financial effects (b, d) have a high cost and that's why they go behind the trust boundary. It's exactly the chatbots guide's table, applied to sub-workflows exposed as tools.
Exercise 3 — Compatible or breaking, tool version. For check-credit exposed as an agent tool, decide whether each change is compatible or breaking relative to how the agent uses it, applying lesson 6's same test:
(a) Adding an optional include_history parameter with default false.
(b) Rewriting the Description so it now says the tool also serves to issue refunds.
(c) Changing the amount parameter's type from number to string.
See solution
(a) Compatible. An optional parameter with a default; the agent that doesn't send it gets the usual behavior. It doesn't change when or how the agent chooses the tool.
(b) Breaking (in the agent's behavior). Rewriting the Description to cover refunds makes the agent start considering check-credit for refund requests —exactly the overlap that causes it to choose the wrong tool—. It changed which requests trigger the tool. It's a contract break, even though it touches no field: the description is part of a tool's contract.
(c) Breaking. Changing a parameter's type is breaking just like in a contract between workflows: the agent and the validation expected a number, and now the shape changed. Also, an amount as text reintroduces the silent number-disguised-as-text error.
Why this works: lesson 6's test —"does the caller that doesn't find out keep working the same?"— applies identically, with the nuance that here "the caller" is a model and one of the contract's pieces is natural-language text. Changing that description (b) is just as breaking as changing a field, because the description is what the model uses to decide. Contract versioning isn't just for fields; it's for everything the caller —human, workflow, or model— uses to act.
Summary and next step
In this lesson you carried the contract over to a new caller: a model. You saw the diner who only has the menu —an agent that doesn't read your nodes, can't ask, and decides by reading only the contract's text—, and understood that for that caller the name, the description, and each parameter's description are the complete contract. You learned the two paths for exposing a workflow as a tool: a sub-workflow connected to an AI Agent inside n8n (with the "Call n8n Workflow Tool" / "Custom n8n Workflow Tool" node, depending on your version), and your instance as an MCP server for external clients (Claude Desktop, Cursor, ChatGPT) via the MCP Server Trigger with its authenticated URL. You built a tool's contract —a specific Name, a Description saying what it does and when NOT to use it, parameters with $fromAI() anchoring each value to its source— and saw in the worked example that this text is functional: the difference between an agent that chooses well and one that confuses check-credit with issue-refund came from the contract, not the model. And you confirmed nothing you built in this module becomes optional when the caller is an agent: boundary validation matters more (the model infers its inputs and can get them wrong), the trust boundary and idempotency protect money-moving effects, and versioning also applies to the description, because changing it changes how the agent uses the tool.
Before moving on to lesson 8 you should be able to: write the contract for a sub-workflow exposed as a tool, with a description that doesn't overlap another; decide whether a tool goes straight to the agent or behind human review; and classify a change to a tool as compatible or breaking.
You now have every piece of the module: what a contract is, how it's designed, where the boundary lives, how it's validated, how it's versioned, and what it looks like when an agent consumes it. Lesson 8 pulls them together into a single deliverable: you're going to build check-credit end to end —with its documented contract, its boundary validation rejecting invalid inputs, and a second compatible version—, the validated, contract-bearing sub-workflow that's this whole module's exit skill.
Resources
- MCP Server Trigger — n8n Docs — the node that turns your instance into an MCP server so external clients use your workflows as tools, with its URL and authentication.
- How tools work — n8n Docs — what a tool is for an agent in n8n and how the model decides to use it based on its name and description.
- Use AI for parameters — n8n Docs — the complete reference for
$fromAI(): the four arguments (key,description,type,defaultValue) and their types. - Human-in-the-loop for tools — n8n Docs — the human review pattern protecting a tool's irreversible or financial effects when exposed to the agent.
- AI Agent node — n8n Docs — the node where the tools connector you attach a sub-workflow exposed as a tool to lives.