Lindsay Edwards

Report the whole cycle, not just that there is one

On this page

workman is a tool I built for wiring AI agents into workflows: you define agents in YAML, chain them into a directed acyclic graph, and run them across whichever LLM provider fits. Steps declare what they depend on, so the whole thing is a directed graph and the engine has to run the steps in dependency order. That means two classic problems: figure out the order, and refuse to start if the dependencies form a cycle (because a cycle has no valid order, it is a deadlock waiting to happen). The interesting part was not detecting the cycle. It was what the error said when it found one.

A boolean is a bad error message#

The standard way to order a dependency graph is a topological sort, and a clean way to write one is a depth-first traversal that colours each node three ways: not yet visited, currently on the stack, or fully done. If you ever reach a node that is already “on the stack,” you have found a back edge, which is to say, a cycle.

The lazy version stops there and throws “a cycle exists.” Which, in a graph of thirty steps, is close to worthless. Somewhere in here, two or more of your steps depend on each other, good luck. You are now doing the graph traversal in your head that the computer just did and threw away.

For anyone actually depending on that workflow to run, a vague “a cycle exists” error is the difference between a two-minute fix and an afternoon spent staring at thirty steps trying to spot which two of them accidentally depend on each other.

The better version keeps the current path as it descends, and the moment it hits a node already on the stack, it slices the path from that node onward and reports the actual loop: a depends on b depends on c depends on a. Same detection, but now the error hands you the exact chain to break. The information to produce it was right there in the traversal; the only difference is choosing to keep it and put it in the message.

The algorithm already knows the answer at the moment it fails. A good error is mostly the discipline of not throwing that knowledge away on the way out.

Cheap checks go before the expensive one#

There was a second lesson stacked on top, about ordering your validations. The topological sort assumes the graph is well-formed. Feed it a step that depends on a step that does not exist, and it falls over in a confusing way that has nothing to do with the real problem, which is the dangling reference.

So the structural checks run first, and separately: does every dependency name a real step, does anything depend on itself. Only if those pass does the cycle detection run. Cheap checks that produce clear, specific errors (“step X depends on unknown step Y”) go before the expensive traversal that assumes everything is already sane. Otherwise a simple typo in a dependency name surfaces as a baffling failure deep inside the graph algorithm, and you debug the wrong thing.

The transferable bit#

Neither of these is really about graphs. The first is a rule about errors: when something fails, spend the extra line to report what failed specifically, because the code almost always has that detail at the point of failure and usually discards it. The second is a rule about validation order: run the cheap, specific checks before the powerful, assumption-heavy ones, so a small mistake gets a small, clear error instead of detonating somewhere confusing downstream. Both come down to the same courtesy, aimed mostly at future-you at two in the morning: fail in a way that points at the fix.

Keep reading