Module 5: Dependencies Between Workflows
3. The dependency graph
Description
By the end of this lesson you will be able to draw the dependency graph of a multi-workflow system: a diagram where each box is a workflow, each arrow is a "this depends on that" relationship, and each arrow is annotated with its type — synchronous call, asynchronous fire, or event — and with whether it carries a read or an effect. With that drawing in hand you will be able to do three things that are guesswork without it: detect cycles (a piece that ends up depending on itself), calculate a failure's blast radius (how far it propagates if a piece goes down), and uncover hidden dependencies — pieces that do not call each other but share a resource and therefore go down together.
This matters because the graph is the cheapest, most powerful tool in the entire module. It requires no code, it gets drawn in five minutes, and half the coordination bugs you would see show up in production are visible at a glance in it: a cycle jumps out at you, a point that if it goes down leaves three pieces without service is visible in a single look, a hidden dependency gets uncovered as soon as you note which external resource each piece touches. Systems that blow up in production almost never blow up because of a misconfigured node; they blow up because of a relationship between workflows that no one drew and therefore no one saw.
Connection to the module: lesson 2 gave you the two arrow types — orchestration (a call that waits) and choreography (an event that reacts). This lesson puts all the arrows together in a single drawing and teaches you to read it. It is the diagnostic tool you are going to use for the rest of the module: in lesson 4 the graph shows you where you split and rejoin work, in lesson 5 where events pile up, in lesson 6 where it makes sense to place an outbox, and in the lesson 8 project the graph is, literally, half the deliverable. Learning to draw it well here pays off in every lesson that follows.
A map of "who goes down if this goes down"
Think of a house's electrical panel. It's a row of breakers, and each one feeds a part: one the kitchen, one the bedrooms, one the garage. Most people don't understand it until the day half the house loses power and they discover, fumbling around, that the fridge and the microwave were on the same circuit, so when the microwave's breaker tripped, the fridge went off too and the food spoiled. No one had ever drawn what each breaker fed. The dependency existed — the fridge depended on the same circuit as the microwave — but it was invisible until it caused damage.
A serious electrician doesn't work that way. Before touching anything, they have — or draw — the panel diagram: what breaker feeds what, and what things hang off the same circuit. With that diagram, the question "if I turn off this breaker, what loses power?" has an answer before you turn it off. And the question "if this circuit overloads, what goes down with it?" does too.
The dependency graph is that diagram, for your workflow system. It answers the same question: if this piece goes down, what goes down with it? And its value is the same: it turns an invisible dependency — one you'd only discover the day of the blackout — into a drawn line you can see, reason about, and fix before it causes damage. A system without its graph drawn is a house without a panel diagram: it works until one day half the house loses power and no one knows why.
Anatomy of the graph: boxes, arrows, and annotations
A dependency graph has three ingredients. Let's go one by one, because the power is in the annotations, not the boxes.
Boxes are workflows. Each workflow in the system is a box with its name. In Cumbre: order-triage, check-credit, issue-refund, inventory-sync. Simple.
Arrows are dependencies. An arrow from A to B means "A depends on B": A needs B to do something for A to do its job. Direction matters and is confusing at first, so pin it down carefully: the arrow points toward the piece being depended on. order-triage → check-credit means "order-triage depends on check-credit," that is, order-triage calls it and needs its response. The arrowhead points at whoever does the requested work.
Annotations are what makes the graph useful. A bare arrow tells you there's a relationship, but not what kind. Two annotations change everything:
- The relationship type, which comes from lesson 2: is it a synchronous call (orchestration with waiting: A calls B and sits there waiting for its result)? An asynchronous call (A fires B but doesn't wait)? Or an event (choreography: A emits an event and B reacts)? Each type propagates differently, as you'll see.
- What it carries, which comes from the whole guide: is the piece being depended on a read (safe to repeat, like
check-credit) or an effect (dangerous to repeat, likeissue-refundandinventory-sync)?
We're going to use simple notation for the arrows. I write it like this so you can draw it by hand:
A ──(sync)──▶ B A calls B and waits for its result (synchronous orchestration)
A ┈┈(async)┈▶ B A fires B and keeps going without waiting (asynchronous orchestration)
A ~~(event)~▶ B A emits an event; B reacts (choreography)
And we mark each destination box with [L] if it's a read or [E] if it's an effect.
Cumbre's graph, drawn out
Let's take lesson 2's orchestrated system and draw the whole thing. order-triage receives the order, checks credit, and depending on the result discounts inventory or issues a refund. It also emits an "order.created" event that triggers the sales notification in choreography.
order-triage
(receives and coordinates)
│
┌─────────────────────┼──────────────────────────┐
│ (sync) │ (sync, if no credit) │ (event)
▼ ▼ ▼
check-credit issue-refund sales-notifier
[L] [E] [E]
(reads credit) (moves money) (sends notice)
│
│ (sync, if there is credit)
▼
inventory-sync
[E]
(changes stock)
That drawing, with its annotations, already tells you a lot more than "there are four workflows." Read it:
order-triagedepends oncheck-creditwith a synchronous call to a read. It waits for the result to decide. Becausecheck-creditis a read, calling it twice does no duplicate damage — but there is cascade risk, becauseorder-triagewaits.order-triagedepends onissue-refundandinventory-syncwith synchronous calls to effects. These are the dangerous arrows: they carry operations that do harm if repeated.order-triagedepends onsales-notifierwith an event. Decoupled: ifsales-notifiergoes down,order-triageneither finds out nor gets blocked.
Notice how the annotations direct your attention. The two synchronous arrows toward effects are where the module's risk lives. The event arrow is the calm one. The arrow toward the read is calm regarding duplicates but is the cascade suspect. A well-annotated graph is a map of where to look.
Direct and transitive dependencies
There's a subtlety the graph makes visible and that gets lost at a glance: transitive dependencies.
A direct dependency is an arrow: order-triage → check-credit. A transitive dependency is a chain: if check-credit, in turn, depended on a credit-bureau-lookup workflow that queries an external credit bureau, then order-triage would depend transitively on credit-bureau-lookup, even though it doesn't call it directly.
order-triage ──(sync)──▶ check-credit ──(sync)──▶ credit-bureau-lookup [L]
Why does it matter? Because the blast radius follows the transitive chains, not just the direct arrows. If credit-bureau-lookup goes down, check-credit can't return its result, and order-triage — which waits on check-credit — doesn't move forward either. The credit bureau, which order-triage doesn't talk to directly and probably doesn't even know exists, can stall order intake. That's the kind of surprise the graph prevents: you draw the full chain and see, all the way to the end, what you actually depend on.
The practical rule: your system depends on everything downstream of your arrows, not just what you touch directly. The transitive graph — the one that follows each chain to the end — is the one that tells you your real failure surface.
Cycles: a piece that depends on itself
A cycle is when you follow the arrows and come back to the starting point: A depends on B, B depends on C, C depends on A. In a dependency graph, a cycle is almost always a problem, and sometimes a disaster.
Think of two people yielding the right of way at a door with exaggerated politeness. "After you." "No, after you." "I insist, you first." If neither one breaks the yielding rule, they stay there forever. Each one is waiting on the other; no one moves forward. That's a synchronous cycle: a deadly embrace where each piece waits on the one waiting on it.
In Cumbre, a cycle could sneak in like this, with no bad intent. Suppose someone decides that inventory-sync, when it detects a product left at negative stock because of an error, should trigger issue-refund to return the money for the units that weren't there. And that issue-refund, to know how much to refund, calls inventory-sync to check the stock. Drawn out:
issue-refund ──(sync)──▶ inventory-sync ──(sync)──▶ issue-refund ──▶ ...
If both calls are synchronous and wait, you have the deadly embrace: issue-refund waits on inventory-sync, which waits on issue-refund, which waits... No one finishes. And if instead of waiting, each one fires the other, you have something worse: a self-feeding loop, issuing refunds and syncing inventory in a chain, without end, until something breaks or until you've issued a hundred refunds.
How a cycle is detected. By eye, in a small system, it's visible: you draw the arrows and notice you're back at a box you already passed through. In a bigger system, the most reliable way is exporting the workflows to JSON and following the calls — each Execute Sub-workflow tells you who it calls — or simply building the graph piece by piece and checking whether any chain bites its own tail. The rule is hard and worth memorizing: in a healthy dependency graph, if you start at any box and follow the arrows, you never come back to that box. That's called an acyclic graph, or a graph with no cycles.
How a cycle gets broken. Almost always, a cycle is a sign that two pieces are too entangled and one of the two relationships shouldn't exist, or should be an event instead of a call. In the example: instead of issue-refund calling inventory-sync to check the stock, the stock should travel in the assignment — order-triage or another director passes issue-refund how much to refund — cutting that arrow. Also remember the rule you saw in the chatbots-for-agents guide: workers don't delegate to each other directly, precisely so the graph has no cycles. The same discipline applies to workflows.
Hidden dependencies: pieces that go down together without calling each other
Here's the kind of dependency that causes the most damage, because it isn't an arrow. It's two pieces that, in the call graph, don't touch each other — neither calls the other — and yet go down together, because they share a resource.
Back to the electrical panel: the fridge and the microwave don't "call" each other, but they share a circuit, so when the circuit trips, both go off. In a workflow system, the shared resources that create hidden dependencies are several:
- A shared external API. In Cumbre, both the order's original charge and
issue-refunduse the same payment gateway. There's no arrow between them in the call graph. But if the gateway goes down, or if you hit its rate limit, both fail at once. They share the gateway the way the fridge and the microwave share the circuit. - A shared database. Cumbre's four workflows read and write the same Postgres ledger. If that database gets saturated or goes down, all four are affected, even though they don't call each other.
- A shared credential or quota. If
check-creditand another workflow use the same API key for a service with a monthly limit, one can exhaust the quota and leave the other without service, with no call existing between them. - The same worker. On an instance without queue mode, all workflows share the same process. A workflow that consumes all the CPU affects the others. Lesson 5 comes back to this.
Hidden dependencies get drawn separately, because they aren't call arrows. A useful form is to note, under each box, which external resources it touches, and then mark the ones that repeat:
order-triage → payment gateway (charge), Postgres ledger, AI model
check-credit → credit bureau API, Postgres ledger
issue-refund → payment gateway (refund), Postgres ledger
inventory-sync → warehouse system, Postgres ledger
Shared resources (hidden dependencies):
⚠ payment gateway → order-triage + issue-refund go down together if the gateway fails
⚠ Postgres ledger → all four depend on it
That small resource inventory, done once, saves you the blackout. The first time the payment gateway has a bad day, you'll already know — because you drew it — that new order charges and refunds will fail at the same time, and you'll be able to explain it instead of fumbling around.
Worked example: the blast radius of two failures
The graph earns its full value when you use it to answer "what goes down if this goes down?" Let's calculate it for two concrete Cumbre failures, using the graph above.
Failure 1 — check-credit goes down. The credit bureau system stops responding, so check-credit can't return its result.
What to expect. We follow the arrows backward, toward whoever depends on check-credit. The only arrow reaching check-credit comes from order-triage, and it's a synchronous call: order-triage is waiting for the result. Since it doesn't arrive, order-triage stays blocked — or fails, if it has a timeout configured — at that point. And because order-triage doesn't move forward, it never gets to call inventory-sync or issue-refund either: the entire chain downstream of the credit decision goes unexecuted. The blast radius is: order-triage (blocked) and, transitively, everything order-triage would do afterward. sales-notifier, on the other hand, which hangs off an independent event, is not affected: it reacts to "order.created," which was emitted before the credit chain. There you see, in the graph, the difference between a synchronous arrow (propagates the failure) and an event one (contains it).
Failure 2 — the payment gateway goes down. This one is the interesting one, because it's a hidden dependency, not an arrow.
What to expect. The gateway doesn't show up as a box in the call graph, so if you only looked at the arrows, you'd say "this affects nothing." But in your resource inventory you noted that two pieces use it: the charge inside order-triage and issue-refund. When the gateway goes down, both fail at once: new orders can't be charged and refunds can't be issued. Two symptoms that seem unrelated — "orders aren't coming in" and "refunds aren't going out" — have the same root cause, and you only know that because you drew the hidden dependency. Without that inventory, you'd have chased two separate bugs for an hour before realizing it was just one.
The lesson from the two failures: blast radius is followed through synchronous arrows backward (who's waiting on me) and through shared resources (who shares my circuit). Event arrows cut the propagation. And hidden dependencies are only visible if you noted them. A graph with only call arrows is half-drawn.
How to build the graph of a real system, step by step
When you have to do this for a system you didn't design yourself — the most common case in real work — here's the procedure:
- List the workflows. One box per workflow. In n8n, your instance's workflow list is the starting point.
- Find the call arrows. Open each workflow and look for
Execute Sub-workflownodes: each one is an arrow toward the workflow it calls. Note whether it hasWait for Sub-Workflow Completionturned on (synchronous) or not (asynchronous). - Find the event arrows. Look for workflows that emit events — an
HTTP Requesttoward another one's webhook, or a write into an events table — and the ones that receive them — aWebhookor aSchedule Triggerthat polls that table. Each emitter-receiver pair is an event arrow. - Note reads and effects. For each destination box, mark
[L]or[E]depending on whether its job is a read or an effect. You know this from Module 1's audit. - Inventory the external resources. Under each box, list the APIs, databases, and credentials it touches. Mark the ones that repeat: those are your hidden dependencies.
- Look for cycles. Start at any box, follow the arrows, and check that you never come back to a box you already passed through.
When you're done you have a drawing that answers, without running anything, the questions that matter: what goes down if each piece goes down, where there's a deadly embrace waiting to happen, and which pieces share a circuit. That drawing is half of lesson 8's deliverable, and it's the first thing a system owner does when they inherit an automation they don't understand.
Common mistakes
Drawing only the call arrows and forgetting shared resources (conceptual). What happens: someone makes a tidy graph with every Execute Sub-workflow arrow, concludes "there are no dependencies between order-triage and issue-refund other than through the director," and the day the payment gateway goes down, both fail at once and no one had foreseen it. Why it happens: call arrows are visible — there's a node that represents them — and shared resources aren't; you have to go looking for them on purpose. How to detect it: if your graph doesn't have a list of external resources per box, it's half done. How to fix it: always do step 5 of the procedure — inventory the external resources and mark the repeated ones; hidden dependencies cause the most confusing failures precisely because they don't show up in the arrows.
Confusing the arrow's direction (practical). What happens: someone draws check-credit → order-triage because "check-credit gives the result to order-triage," and then miscalculates the blast radius, because they followed the arrows backward. Why it happens: a dependency's direction is counterintuitive — the data travels from check-credit to order-triage, but the dependency goes the other way, because it's order-triage that needs check-credit. How to detect it: ask yourself "which of the two can't do its job without the other?" That's the one depending, and the arrow comes out of it. How to fix it: fix the convention once and for all — the arrow points toward the piece being depended on, the one doing the requested work — and check it by reading the arrow out loud: "order-triage depends on check-credit," arrow from order-triage to check-credit.
Ignoring transitive dependencies (conceptual). What happens: someone maps their system's direct arrows, feels covered, and doesn't notice that a workflow three hops downstream — one their director never calls directly — is a slow external credit bureau that can stall everything. Why it happens: it's natural to stop at "who do I call" and not follow the chain to the end. How to detect it: for every synchronous arrow, ask yourself "and who does this one depend on, in turn?" and keep going until you reach pieces that don't call anyone. How to fix it: draw the full, transitive graph, down to the leaves; your real failure surface includes everything downstream, not just what you touch directly.
Not checking for cycles until one blows up (practical). What happens: arrows keep getting added to the system over time — "let this one also call this one" — and at some point a cycle closes without anyone noticing, until the day two workflows end up waiting on each other or enter a loop that issues refunds without stopping. Why it happens: each arrow gets added for a good local reason, and the cycle emerges from the sum, which no one looks at as a whole. How to detect it: every time you add a new arrow, walk the graph from the destination box, following the arrows, and check that you can't get back to the origin box. How to fix it: keep the graph acyclic as a design rule; if a new arrow would close a cycle, it almost always means that relationship should be an event, or that a piece of data being requested in a call should travel in the assignment instead.
Exercises
Exercise 1 — Draw and annotate. Cumbre adds a new workflow, restock-alert, which fires when inventory-sync leaves a product below its minimum: inventory-sync emits a "stock.low" event and restock-alert reacts by sending an email to the vendor. restock-alert is an effect (it sends an email). Draw Cumbre's complete graph including this piece, with arrow-type notation and [L]/[E], and say what order-triage now depends on transitively that it didn't before.
See solution
order-triage
│
┌─────────────────────┼──────────────────────────┐
│ (sync) │ (sync, if no credit) │ (event)
▼ ▼ ▼
check-credit [L] issue-refund [E] sales-notifier [E]
│
│ (sync, if there is credit)
▼
inventory-sync [E]
│
│ (event: "stock.low")
▼
restock-alert [E]
Before, order-triage depended transitively on check-credit, issue-refund, and inventory-sync. Now, because inventory-sync emits an event that triggers restock-alert, there's a new chain. But notice the important detail: that new arrow is an event, not a synchronous call. So order-triage does not depend on restock-alert in a way that could block it: if restock-alert goes down, inventory-sync already emitted its event and moved on, and order-triage never even finds out. The chain exists in the graph, but the event arrow decouples it. That's why noting the arrow type isn't decorative: it completely changes how a failure propagates along that chain.
Why this works: you told apart a transitive dependency that exists (there's a path from order-triage to restock-alert) from a failure propagation that doesn't exist (the event cuts the chain). That distinction — there's an arrow, but it doesn't propagate — is exactly what the annotations let you see.
Exercise 2 — Find the cycle. You're handed the description of a system: A calls B synchronously; B, to finish its job, calls C synchronously; C, in a certain case, calls A synchronously. Draw the graph, say whether there's a cycle, what happens when that "certain case" fires, and how you'd break it.
See solution
A ──(sync)──▶ B ──(sync)──▶ C ──(sync, in a certain case)──▶ A ──▶ ...
Yes, there's a cycle: starting at A, you follow the arrows and come back to A. It's a synchronous cycle, so it's a potential deadly embrace. When that "certain case" fires, C calls A, but A is still waiting — upstream, in the first call — for B's result, and B is waiting on C, which is now waiting on A. All three end up blocked, waiting on each other in a circle. No one finishes. In practice, this usually shows up as executions that hang, pile up, and eventually exhaust the instance's resources, or as a "call depth exceeded" error if the engine detects it.
How to break it: you have to cut one of the cycle's three arrows, and almost always the culprit is the last one, C → A, because closing the circle is rarely what's actually needed. The usual options: turn that call into an event (C emits an event and A, or another piece, reacts in a new execution, not inside the one already waiting), or redesign so the data C is requesting from A travels in the assignment from the start, eliminating the need for the call. The underlying rule: keep the graph acyclic.
Why this works: you identified the cycle by following the arrows back to the start, explained the block in terms of "each one waits on the one waiting on it," and applied the standard cure — cut the arrow that closes the circle, turning it into an event or eliminating it. That's the complete handling of a cycle.
Exercise 3 — The blast radius of a hidden resource. In Cumbre, it turns out check-credit and a new workflow fraud-check use the same third-party service to verify identity, and that service has a limit of 100 requests per minute shared between both. On a high-volume Monday, fraud-check consumes all 100 requests by itself. Describe what happens to check-credit, how this problem would show up in the graph, and why it's hard to diagnose without having inventoried the resources.
See solution
What happens to check-credit: since fraud-check exhausted the shared service's 100 requests per minute, check-credit's calls to that same service start getting rejected due to the rate limit. check-credit can't finish its job, so it returns an error or falls behind. And since order-triage calls it synchronously and waits, the order chain slows down — a cascade. The cruel detail: check-credit is perfectly fine; the problem was caused by fraud-check, with which check-credit has no call arrow at all.
How it would show up in the graph: it would not show up in the call arrows, because there's none between check-credit and fraud-check. It would only show up in the resource inventory, if you did it: both would have noted "identity verification service (100/min shared limit)," and that repeated resource is the hidden dependency. Marked this way:
check-credit → identity verification service, ledger
fraud-check → identity verification service, ledger
⚠ verification service (100/min shared)
→ fraud-check can exhaust the quota and leave check-credit without service
Why it's hard to diagnose without the inventory: the symptoms point to check-credit ("orders are stalling at the credit check"), but the cause is in fraud-check, a piece that doesn't show up in that part of the graph. Without having noted the shared resource, you'd look for the problem in check-credit and the credit service, and you wouldn't find it, because the culprit is a neighbor sharing an invisible quota.
Why this works: this is the most treacherous kind of dependency — a shared quota — and you resolved it with the only tool that makes it visible: the resource inventory with the limits noted. That's why step 5 of the procedure isn't optional.
Summary and next step
In this lesson you learned to draw the dependency graph of a multi-workflow system: boxes for the workflows, arrows for the dependencies — pointing toward the piece being depended on — and two annotations that make it useful: the arrow type (synchronous, asynchronous, or event) and what it carries (read [L] or effect [E]). With that drawing you can do three things that are guesswork without it: detect cycles — walking the arrows and checking you never come back to a box, the mark of a healthy graph, calculate a failure's blast radius — following the synchronous arrows backward and the shared resources, and uncover hidden dependencies — pieces that don't call each other but share an API, a database, or a quota, and therefore go down together. You saw that event arrows cut failure propagation, that your system depends on everything downstream (transitivity), and that a graph without its resource inventory is half-drawn.
Before moving on to lesson 4 you should be able to: draw Cumbre's graph with its annotations from memory; explain why a synchronous arrow propagates a failure and an event one doesn't; and describe how a hidden dependency — two pieces sharing a resource — causes them to go down together with no arrow between them at all.
Now that you know how to read the graph, lesson 4 dives into a concrete arrow shape that appears when a workflow splits work among several: fan-out — a flow triggers N sub-executions — and fan-in — joining their results back together. You'll see the challenge this poses in n8n, how the Merge node joins what got split, and the specific danger of the partial retry: when out of N branches, some finished and others didn't, and retrying re-runs the ones that were already done. And you'll see how Module 4's ledger — the shared whiteboard — is exactly what covers that gap.
Resources
- Sub-workflows — n8n Docs — how a system is made up of several workflows that call each other; the calls you draw as arrows are born from these nodes.
- Execute Sub-workflow node — n8n Docs — each one of these nodes is an arrow in the graph; its
Wait for Sub-Workflow Completionoption decides whether the arrow is synchronous or asynchronous. - Export and import workflows — n8n Docs — how to export a workflow to JSON to read its connections and calls when building the graph of a system you didn't design.
- Error handling — n8n Docs — the error handling that determines exactly what happens when a downstream piece fails; the blast radius depends on how this is configured, a subject Module 6 goes deeper into.