In this article
  1. Before you start
  2. Why interviewers ask this
  3. Mini workflow engine from first principles
  4. The Staff-level answer
  5. The rubric
  6. Three variants of this question
  7. Common mistakes
  8. Follow-up questions
  9. Sources

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, steps

The 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

A step cap of 50 stops every run at 50 node runsBar chart of node runs executed under a cap of 50: the draft loop uses 6, a workflow needing 50 runs uses 50, and workflows needing 51 runs or looping forever both stop at 50.Node runs executed (runs)0204060Draft loopNeeds 50 runsNeeds 51 runsEndless loopWorkflow6505050Diagram: Interview Clozo, CC BY 4.0 ·interview.clozo.ai/blog/build-a-mini-workflow-engine-mini-langgraph.html
A cap of 50 lets a 50-run workflow finish and stops a 51-run workflow after 50 runs.CC BY 4.0 — source: How do you build a mini workflow engine (mini-LangGraph)?
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"])
A mini engine runs nodes in a loop that counts every stepFlow of four parts: a compile check validates the graph before the run, nodes read the shared state, a run loop counts each node run against the step cap, and edges read the state and return the next node to the loop.Nodes andstateupdate pernodeupdateRun loopcounts,checks capasknext nodeEdgeschoose nextnodebeforethe runCompilecheckbefore therunDiagram: Interview Clozo, CC BY 4.0 ·interview.clozo.ai/blog/build-a-mini-workflow-engine-mini-langgraph.html

The run loop is the heart of the engine: it counts every node run and asks the edge for the next node.CC BY 4.0 — source: How do you build a mini workflow engine (mini-LangGraph)?
Step-by-step description
  1. A node reads state and returns an update.
  2. The loop counts runs and stops at the cap.
  3. An edge reads state and picks the next node.
  4. 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

Interviewer asks

How would you build a minimal workflow engine with branching, looping, a step limit and tests?

A strong answer

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

A strong answer states the counting rule and tests its boundary, while a Staff answer also explains the design choice behind loop protection.
DimensionWeak answerStrong answerStaff answer
CorrectnessCalls functions in a fixed orderRuns nodes, merges state, follows edgesSame, with the cap checked before each run; states whether a step is a node run or a super-step
Loop protectionA bare while True or noneA step counter with an errorExplains why a runtime bound, not cycle detection, fits a looping graph
State handlingOne global dictionaryUpdates merged per keyPer-key reducers; knows an empty update cannot clear an accumulating key
TestsOne happy-path testTests for branch, loop and capBoundary tests, a fake tool node, a compile test
CommunicationStarts coding at onceStates the assumed meaning of a stepLists 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.

Key takeaways
  • 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

  1. Graph API overview (StateGraph, nodes, edges, state, reducers, super-steps) — LangChain documentation (LangGraph, Python); undated page, read 2026-10-26, 26 Oct 2026
  2. Pregel: a system for large-scale graph processing — Google Research (Malewicz et al., SIGMOD 2010; ACM DOI 10.1145/1807167.1807184), 6 Jun 2010
  3. graphlib: functionality to operate with graph-like structures (Python documentation) — Python Software Foundation; undated page, read 2026-10-26, 26 Oct 2026