In this article
Before you start
You need only basic Python. The engine below is about twenty lines, and every other idea is explained from zero.
Why interviewers ask this
A submission that loops forever, or has no test for its step limit, fails the checks a reviewer can run mechanically, so this exercise sorts candidates quickly. Candidates report it as a take-home: a small engine whose steps form a graph and share state, that can branch, loop and call tools, stops at 50 steps, survives infinite cycles and ships with unit tests.1 The rounds that use this kind of exercise are covered in what the interview process is like. For what a full submission looks like, see the take-home assignment example.
A weak submission is a chain of function calls with a global dictionary. A strong one separates the graph, the state and the run loop, stops cleanly at the limit with a named error, and proves each rule with a test. The AI-focused loops appear in AI engineer interview questions and in the forward deployed and applied AI engineer rounds.
Mini workflow engine from first principles
- Workflow engine
- A workflow engine is a program that runs a set of steps, held as a graph, in an order decided while it runs, passing shared data from one step to the next, so that the order can branch or loop depending on the data produced so far.
- Reducer
- A reducer is a function that merges a node's update into one key of the shared state, taking the current value and the new value and returning the stored value, so each key can either overwrite or accumulate.
- Step cap
- A step cap is a fixed upper bound on how many node runs one execution may perform, checked by the run loop before every run, so a workflow that loops forever ends with an error instead of hanging.
In plain words
Think of a kitchen with a recipe card. Each cook does one job, such as chop or season. The card says who goes next, and a bowl on the counter holds the food so far. The analogy breaks in one place. A real recipe card is fixed, but here the next cook can depend on what is in the bowl.
In one precise sentence, a mini workflow engine keeps a shared state, runs one node at a time, and asks an edge which node comes next. A node is a function that reads the state and returns an update. An edge is a rule that picks the next node. The state is the dictionary all nodes share.
The LangGraph documentation puts it as "nodes do the work, edges tell what to do next". Your engine is that idea at small scale.
Step 1: Nodes read the state and return an update
A node takes the current state and returns only what it wants to change. The input is the state and the output is a small dictionary, such as {"score": 40}. A node can hold a large language model (LLM) call or ordinary code; the LangGraph docs say nodes and edges are nothing more than functions.
This is where the "tool-based logic" in the prompt lives. A tool node is a node that calls an outside function, such as a search or a calculator, and writes the result into the state. In tests you give it a fake function so the test needs no network.
The engine does not let a node overwrite the whole state. It merges the update key by key through a reducer. The default reducer overwrites. A list-append reducer accumulates. You now know what runs; next comes how it runs.
Step 2: The run loop counts every node run and stops at the cap
The run loop is a while loop that runs the current node, merges its update, counts one step, asks the edge for the next node, and repeats until the next node is the end marker, a reserved name meaning "stop here". It checks the cap before each run, so a cap of N allows exactly N runs.
END_MARKER = "__end__"
class StepLimitError(Exception):
pass
def run(nodes, edges, start, state, reducers, cap):
current, steps = start, 0
while current != END_MARKER:
if steps >= cap:
raise StepLimitError(f"stopped after {steps} steps")
update = nodes[current](dict(state))
for key, value in update.items():
merge = reducers.get(key, lambda old, new: new)
state[key] = merge(state.get(key), value)
steps += 1
route = edges[current]
current = route(state) if callable(route) else route
return state, stepsThe prompt does not say whether "max 50 steps" counts node runs or something else, so state your assumption: here one step is one node run. LangGraph runs in discrete super-steps, which are rounds in which the active nodes run (Graph application programming interface (API) docs, read 2026-10-26), so a limit on rounds is not the same as a limit on node runs. A workflow that needs exactly 50 runs finishes with 50 steps, and one that needs 51 raises StepLimitError after 50 runs.2 If the check ran after the increment, the last allowed run would be refused.
A draft loop finishes in 6 node runs, a workflow that needs 51 runs stops after 50 runs, and an endless loop stops after 50 runs.2
Text description of the diagram
- A draft loop finishes in 6 node runs.
- A workflow that needs 51 runs stops after 50 runs.
- An endless loop stops after 50 runs.
Step 3: Edges read the state and choose the next node
An edge is either a fixed name or a function of the state. A fixed edge always returns the same node. A conditional edge looks at the state and returns a node name or the end marker. The LangGraph docs say edges can be conditional branches or fixed transitions, and that composing them gives looping workflows.
Branching and looping are the same mechanism. A branch returns one of two names; a loop returns a name already visited. In the draft, critique and revise example, the critique edge returns the end marker when the score reaches the threshold and "revise" otherwise. With an illustrative start score of 40, a gain of 20 per revision and a threshold of 80, the run is draft, critique, revise, critique, revise, critique: 6 node runs.2
This is also why cycle protection is a runtime bound. Loops are legal here, so you cannot reject every cycle in advance; you bound how long one run may go. Static cycle checks belong to engines that allow no loops.
Step 4: Compile checks the graph, and tests pin the behaviour
Compile is a check that runs before the first node. The LangGraph docs describe compiling as a step that checks structure, for example that there are no orphaned nodes. A compile check rejects edges that point to missing nodes, and your version also checks that every node can be reached from the start. A conditional edge needs a declared list of its possible targets so compile can read them.2
Then pin the rules with unit tests. Test the cap on the exact boundary, the loop that ends, the loop that never ends, the reducer, and compile. Three are shown here; the helpers counter(target, cap), draft_loop(cap) and append sit beside run in the data script, which also holds the other two tests.2
import unittest
class EngineTests(unittest.TestCase):
def test_cap_boundary(self):
state, steps = counter(50, 50) # needs exactly 50 runs
self.assertEqual(steps, 50)
with self.assertRaises(StepLimitError):
counter(51, 50) # needs 51 runs, cap is 50
def test_draft_loop_is_six_runs(self):
state, steps = draft_loop(50)
self.assertEqual(steps, 6)
def test_append_reducer_with_empty_list(self):
self.assertEqual(append(["draft"], []), ["draft"])Step-by-step description
- A node reads state and returns an update.
- The loop counts runs and stops at the cap.
- An edge reads state and picks the next node.
- Dangling edges are rejected before running.
- The run loop counts every node run and stops at the step cap.
- Edges read the state and choose the next node.
- A compile check rejects edges that point to missing nodes.
The step-and-halt style comes from Google's Pregel model, which the LangGraph docs credit; see Malewicz et al., 2010. Execution ends when no node is active, which your loop models as reaching the end marker.
Knowing it is half the job. Saying it is the other half.
Interview Clozo is an AI interview copilot for real interview rounds: it hears the question and puts a strong answer on your screen, and you say it in your own words. See the features or how it works.
Start Free Trial →The Staff-level answer
How would you build a minimal workflow engine with branching, looping, a step limit and tests?
I model it as nodes, edges and one shared state. A node is a function from state to an update. An edge is a fixed name or a function of state that returns the next node or the end marker. A run loop merges each update through a per-key reducer, counts one step per node run, and checks the cap before running, so the cap allows exactly that many runs and then raises a named error. Loops are legal, so protection is a runtime bound, not cycle detection. A compile step rejects dangling edges and unreachable nodes. I test the boundary, the reducer and compile.
What makes this Staff-level is that it states the counting convention, explains why loop protection is a runtime bound, and ties every rule to a test. A Staff answer also notes that LangGraph proceeds in super-steps (rounds), not single node runs, so it says whether 50 means node runs or rounds.
The rubric
| Dimension | Weak answer | Strong answer | Staff answer |
|---|---|---|---|
| Correctness | Calls functions in a fixed order | Runs nodes, merges state, follows edges | Same, with the cap checked before each run; states whether a step is a node run or a super-step |
| Loop protection | A bare while True or none | A step counter with an error | Explains why a runtime bound, not cycle detection, fits a looping graph |
| State handling | One global dictionary | Updates merged per key | Per-key reducers; knows an empty update cannot clear an accumulating key |
| Tests | One happy-path test | Tests for branch, loop and cap | Boundary tests, a fake tool node, a compile test |
| Communication | Starts coding at once | States the assumed meaning of a step | Lists assumptions first, then the design |
Three variants of this question
Can you do it with a directed acyclic graph instead of loops?
A directed acyclic graph (DAG) has no cycles, so you can sort it once and run each node once, and Python's graphlib can detect cycles before the run. That is a different engine. The prompt asks for looping, so the cap stays.
What if the cap applies per node instead of per run?
Keep a counter per node name in addition to the total, and raise when either bound is hit. A per-node cap catches a single node that spins while the total allows other work to continue.
What if two nodes may run at the same time?
Run the nodes that are active in one round concurrently, collect all their updates, then merge them through the reducers. This matches the discrete super-steps in the LangGraph docs, and the reducer decides how two updates to one key combine.
Common mistakes
- "I check the cap after each run." (wrong) That refuses the last allowed run or lets one extra through. (right) "I check before each run, so a cap of N allows exactly N runs."
- "I detect cycles in the graph to stop infinite loops." (wrong) Loops are intended, so you would reject valid workflows. (right) "I bound the number of runs at runtime."
- "An empty update resets the field." (wrong) With a merging reducer, the LangGraph docs say returning an empty value does not clear the field. (right) "Clearing needs an overwrite reducer or an explicit reset."
- "Every node must call a language model." (wrong) Nodes can be plain code. (right) "Nodes are functions; a tool node is one that calls an outside function."
Follow-up questions
How do you unit test the step cap?
Build a counter workflow whose edge returns the end marker once a count reaches a target. A target equal to the cap finishes with 50 steps, and a target one higher raises StepLimitError after 50 runs.2 Assert both.
How do you stop an infinite loop before the cap is reached?
Record each (node name, state snapshot) pair before a run. If nodes and edges are deterministic, seeing the same pair twice proves the run will repeat forever, so raise at once. Checking unchanged state alone would stop the valid draft loop, because the critique node changes nothing. A tool node with outside effects breaks that proof, so the cap stays as the last guard.
How does a tool-based node fit in?
The node calls an outside function and returns its result as an update. Pass the function in as an argument, so a test can give a fake one, and treat a tool error as a state value an edge can route on.
What if the cap is lowered so the draft loop no longer fits?
With a cap of 5, the 6-run draft loop raises StepLimitError after 5 runs: the score has already reached 80, but the critique run that would end the workflow is the sixth and is refused.2 The error message should include the step count and the node that was about to run.
Why does an empty update not clear a field?
The LangGraph docs give the example where the state has the tags ["draft"] and a node returns ["review"], and the merged value is ["draft", "review"]. An empty list merged the same way leaves ["draft"] unchanged.2 Related practice appears in the AI-assisted coding interview guide, OpenAI interview questions and the Anthropic interview process.
- A mini workflow engine is nodes, edges and one shared state, driven by a run loop.
- Check the step cap before each run, so a cap of N allows exactly N runs.
- Loops are intended, so loop protection is a runtime bound, not cycle detection.
- Per-key reducers merge updates; an empty update does not clear an accumulating key.
- Compile the graph before running, and test the boundary of the cap.
Sources
- Graph API overview (StateGraph, nodes, edges, state, reducers, super-steps) — LangChain documentation (LangGraph, Python); undated page, read 2026-10-26, 26 Oct 2026
- Pregel: a system for large-scale graph processing — Google Research (Malewicz et al., SIGMOD 2010; ACM DOI 10.1145/1807167.1807184), 6 Jun 2010
- graphlib: functionality to operate with graph-like structures (Python documentation) — Python Software Foundation; undated page, read 2026-10-26, 26 Oct 2026