Scheduler Agent Supervisor β Part 1: Foundations & Core Concepts
maang.io System Design Series
What is this?
Picture a general contractor renovating a kitchen. The job isn't one action β it's a chain of steps that must all land, in order, and each one is handed off to some flaky part of the outside world. The plumber has to move a water line. The electrician has to run a new circuit. The tiler can't start until both are done and the inspector has signed off. Any of them might show up late, botch the work, or simply not show up at all.
A good contractor doesn't hold all of this in their head. They do three things:
- A foreman draws up the ordered plan and writes every step's status into a logbook bolted to the wall β "plumbing: in progress since 9am," "electrical: done," "tiling: not started."
- Each step is dispatched to a subcontractor who goes and does that one job against the messy real world.
- A site inspector walks in every morning, reads the logbook, and looks for trouble: a step marked "in progress" for three days (stuck), or "failed." For each one they decide β send the sub back to try again, or, if it simply can't be finished, order the completed work torn out so the kitchen isn't left half-demolished.
The magic is the logbook on the wall. If the foreman quits mid-project, a new foreman reads the logbook and picks up exactly where things stood. The inspector never needs to have been there yesterday β the wall tells the whole story. Nothing lives only in someone's memory, so nothing is lost when a person disappears.
That is the Scheduler Agent Supervisor pattern, almost line for line. The foreman is the Scheduler, the subcontractors are Agents, the inspector is the Supervisor, and the logbook on the wall is a durable state store. It's a cloud-resiliency pattern for running a multi-step distributed job so that it either completes fully or is cleanly remediated β never left stranded halfway.
The one-line idea: Keep the state of a long, multi-step operation in a durable store β the single source of truth β so that one component drives the steps forward, another does the actual (unreliable) remote work, and a third independently watches for stalled or failed steps and either retries them or undoes the work. No step's fate ever depends on a process staying alive.
We'll use one running example throughout all three parts: booking a trip β reserve a flight, then a hotel, then a rental car, each through a different third-party service that can be slow, flaky, or down.
1. Why this pattern exists β what was painful before
Say you write the trip-booking flow the obvious way: one function that calls the flight API, then the hotel API, then the car API, catching errors as it goes.
This looks fine and works in the demo. In production it has three deep problems:
- Partial failure leaves a mess. The flight and hotel succeed, then the car API times out. Now the customer is charged for two of three bookings, and there's no record anywhere of what to do about it. A naive
try/catchmight attempt to cancel the flight and hotel β but what if that cancel call also fails? - The orchestrator itself can die. Your process crashes right after
reserveHotel()returns. When it restarts, it has no idea a booking was ever in flight. The flight and hotel reservations are now orphaned, paid-for ghosts. The state lived only in a stack frame that no longer exists. - Blind retries double things up. The hotel call times out, so you retry it β but the first call actually succeeded on the hotel's side; the timeout was just the response getting lost. You've now booked (and charged for) two hotel rooms.
Couldn't a database transaction fix this? No β and this is the crux. A classic ACID transaction (the ACID chapter) or two-phase-commit (the Two-Phase Locking topic covers the locking cousin) needs all participants to speak one transaction protocol and hold locks until everyone commits. Third-party flight and hotel APIs won't enlist in your transaction, and you can't hold a lock across a service that takes 30 seconds to answer. You cannot wrap the outside world in a transaction. So instead of guaranteeing atomicity with locks, you guarantee eventual completion or clean remediation with durable state plus an independent watcher. That's the whole pattern.
2. Core concepts & vocabulary β the mental model
Four moving parts. Three are active components; the fourth, the state store, is where they all meet.
The Scheduler (the foreman). It knows the plan β the ordered list of steps that make up the business operation β and it drives that plan forward. Its most important job isn't calling services; it's recording state. Before and after each step it writes to the durable store: "flight step: Running, must finish by 10:00:30," then later "flight step: Completed." The Scheduler is the only thing that knows step 2 comes after step 1.
The Agents (the subcontractors). Each Agent encapsulates a single call to one remote service. The Flight Agent knows how to talk to the flight API β its quirks, its auth, its ret/timeout behavior β and nothing else. Agents exist to isolate the Scheduler from the unreliable outside world: the Scheduler says "do the flight step," and the Agent deals with the timeouts, the retries-in-the-small, and the translation. Crucially, an Agent applies a timeout to its remote call so it can never hang forever, and it reports the outcome back into the state store.
The Supervisor (the inspector). This is what makes the pattern resilient rather than just organized. The Supervisor runs on its own schedule, independent of the Scheduler, and does one thing: scan the state store for steps that have gone wrong β steps stuck in "Running" past their deadline (the Agent or Scheduler must have died), or steps marked "Failed." For each, it decides the remedy: retry (if the failure looks transient and we haven't retried too many times), or remediate/compensate (undo the steps that did complete, so the system returns to a clean state). The Supervisor never does the work itself β it re-triggers Agents or kicks off compensation. It's the safety net, not the acrobat.
The durable state store (the logbook). A persistent store β a database or table β that holds the state of every task and every step. This is the single source of truth. Every component reads and writes here; nobody trusts in-memory state. Because the store is durable, a crash of any component loses nothing β the survivor reads the wall and carries on. (Good fits: a table store like the DynamoDB or MongoDB topics describe, or any store that supports atomic conditional updates β we'll see in Part 2 why that "conditional" matters enormously.)
2.1 Idempotency β the concept that makes it all safe
Because steps get retried (by the Agent, by the Supervisor, by an at-least-once message queue), every operation must be idempotent β running it twice must have the same effect as running it once. "Reserve hotel room #204 for idempotency key abc-123" is safe to send five times: the hotel service records the key, honors the first one, and returns the same confirmation for the rest. "Charge this card $200" is not idempotent unless you make it so with such a key. Idempotency is what turns dangerous retries into safe ones, so we bake it in from the start β and we'll spend real time on it in Part 2.
π‘ A quick vocabulary anchor: a compensating transaction is the "undo" for a completed step β cancel the flight, refund the room. It's a semantic undo, not a rewind: you can't un-send a confirmation email, so compensation sends a correction ("your booking was cancelled"). This is the same idea the Distributed Transactions (Saga) topic builds on, and Part 3 pins down exactly how the two patterns relate.
3. How you actually use it β a concrete peek
You don't install "a Scheduler Agent Supervisor." It's a design, and its heart is the shape of the record you keep in the durable store. Here's the state for one in-flight trip booking β this single document is the "logbook entry" all three roles read and write:
{
"taskId": "trip-9f3a",
"state": "Running", // Running | Completed | Compensating | Compensated | Failed
"createdAt": "2026-07-05T10:00:00Z",
"steps": [
{
"name": "reserveFlight",
"agent": "flight-agent",
"state": "Completed", // NotStarted | Running | Completed | Failed
"idempotencyKey": "trip-9f3a-flight",
"completeBy": "2026-07-05T10:00:30Z", // the "lease" the Supervisor watches
"retryCount": 0,
"result": { "confirmation": "FL-88213" }
},
{
"name": "reserveHotel",
"agent": "hotel-agent",
"state": "Running", // in flight right now
"idempotencyKey": "trip-9f3a-hotel",
"completeBy": "2026-07-05T10:01:15Z",
"retryCount": 1
},
{ "name": "reserveCar", "agent": "car-agent", "state": "NotStarted",
"idempotencyKey": "trip-9f3a-car", "retryCount": 0 }
]
}
Read it like the inspector would: the flight is booked, the hotel is in progress and must finish by 10:01:15 (it's already on retry #1), and the car hasn't started. If the clock passes 10:01:15 and the hotel step is still "Running," the Supervisor knows something died and steps in. Notice every step carries its own idempotencyKey β that's the token that keeps a retry from double-booking.
The lifecycle in one breath:
4. When to reach for it β and when not
Reach for Scheduler Agent Supervisor when:
- You have a long-running, multi-step operation spanning several independent, unreliable services (especially third parties you don't control).
- You need a strong guarantee that the operation either completes or is cleanly undone β no permanent half-states, no orphaned charges.
- The orchestrator itself might crash, and recovery must be automatic. This is the differentiator: durable state + an independent Supervisor means no human has to notice and clean up.
- Steps can be made idempotent (or you can add an idempotency/dedupe layer), so retries are safe.
Reach for something else when:
| Situation | Better tool |
|---|---|
| It's all one database, one process | A plain ACID transaction β atomicity for free (the ACID chapter). Don't build a distributed pattern for a local problem. |
| You mostly need the undo semantics, less the crash-recovery machinery | The Saga pattern directly (the Distributed Transactions topic) β Scheduler Agent Supervisor is essentially a resilient, orchestrated implementation of a saga; Part 3 draws the exact line. |
| You'd rather not hand-build the durable state, timers, retries, and recovery | A workflow engine β Temporal, AWS Step Functions, Azure Durable Functions. These are this pattern, productized. Today that's usually the right call; understanding the pattern is how you understand what they do for you. |
| Individual calls to a flaky service just need to fail fast | The Circuit Breaker pattern β often used inside an Agent, not instead of this pattern. They compose. |
| The work is fire-and-forget, best-effort, no clean-up needed | A simple queue + worker. Don't pay for guarantees you don't need. |
β οΈ The most common mistake is building this pattern from scratch in 2026 when a workflow engine would hand it to you for free. The second most common is the opposite β reaching for a distributed resiliency pattern when a single database transaction would do. Match the machinery to the actual blast radius of a failure.
5. Key takeaways
- Scheduler Agent Supervisor coordinates a multi-step distributed job so it either completes fully or is cleanly remediated β the cloud answer to "I can't wrap third-party services in a transaction."
- Three roles plus a durable state store: the Scheduler plans and records step state, the Agents each do one unreliable remote call (with a timeout), and the Supervisor independently watches for stuck or failed steps and triggers retry or compensation. The store is the single source of truth β so a crash of any component loses nothing.
- Idempotency is foundational, not optional. Retries are inevitable (agents, supervisor, at-least-once queues), so every step and every compensation must be safe to run twice β usually via an idempotency key.
- Compensation is a semantic undo, not a rewind β cancel/refund/correct, in reverse order, for the steps that did complete.
- Reach for it on long, multi-service operations needing guaranteed completion or clean rollback with automatic crash recovery. Skip it for single-database work (use a transaction) β and prefer a workflow engine (Temporal, Step Functions) over hand-rolling it.
Next, Part 2 opens the hood: the exact data structures in the state store, the execution path and the recovery path step by step, how leases and atomic conditional writes stop two components from stepping on each other, the idempotency mechanics, and a fully worked trip-booking failure β with numbers.