---
title: "Task System"
description: "Big goals break into a dependency graph of small tasks, persisted to disk: each node waits on what blocks it, and finishing one unlocks the next."
source: https://vizlearn.app/ai/agents/task-system
access: premium (interactive player requires Lifetime; this doc is free)
---

# Task System

A task system is how an agent survives its own death: the harness decomposes a big goal into small tasks with dependency edges, writes that graph to disk, and re-saves it at every completion. Execution order falls out of the edges — a task runs only when every dependency is done — and a crash costs only the in-flight task.

Compare that to the way an agent "plans" by default: a to-do list living in `messages[]`. It works, right up until the process dies — and processes die constantly. The terminal closes, the machine sleeps, the API errors out, someone hits Ctrl-C. An in-memory plan doesn't just lose its place; it loses the *evidence that anything happened at all*. The work was done, tokens were spent, and the next process starts from a blank slate. In this lesson's run the same crash finishes with 5 completed executions on disk and 7 in memory — the two extra are t1 and t2, paid for twice.

The fix is embarrassingly old technology: a file. `tasks.json` holds the graph — five tasks, their edges, their statuses — and `save` runs inside the loop, right after each `task.status = "done"`. That placement is the whole design. The plan is persisted once; the *progress* is persisted continuously; and a restarted agent's first move is `load`, not `decompose`.

This lesson builds on [the agent loop](https://vizlearn.app/ai/agents/agent-loop) (the executor here is that loop with a scheduler bolted on) and pairs with [subagents](https://vizlearn.app/ai/agents/subagent) — that lesson splits work across *contexts*, this one splits it across *time*.

## How a Task System Works: Edges Gate, Completions Persist

Two lines of the disk-mode loop carry everything: `next_ready(graph)` and the `save("tasks.json")` that follows every completion.

First, ordering. `next_ready(graph)` returns a task whose dependencies are all `done`. In this lesson's workload, *scaffold the module* (t1) unlocks both *implement parser* (t2) and *write tests* (t3); *run test suite* (t4) is a join that needs both t2 **and** t3; *release notes* (t5) waits on t4. Nobody sorts this list. The agent doesn't exercise discipline. The order is a property of the data: when t2 finishes but t3 hasn't, t4 simply isn't in the ready set, and the loop can't pick it. Two tasks legal at once (t2 and t3 after the fan-out) is fine — this single-worker loop breaks the tie by lowest id; a fancier harness would run them in parallel.

Second, durability. `load("tasks.json")` runs *before* `decompose` — resuming beats re-planning — and `save` runs after every completion. Note what save never records: a `running` status. The file is written at decompose time and at completion time, so it always describes a consistent world of "completion recorded" and "completion not recorded." A task that was mid-execution when the process died is, as far as the file knows, still pending. That's not sloppiness — it's the correct answer, because the checkpoint cannot certify completion. Repeating real side effects requires idempotency or a recovery protocol. The restart re-runs exactly that one task.

The memory variant runs the same task graph without durable `load` and `save` boundaries. Nothing else differs. That persistence boundary is the entire lesson.

## Task System Step by Step: One Crash, Two Tasks Saved

The default run uses the disk store and kills the process mid-run. Watch the panel on the right — it mirrors the *file*, not the screen.

1. **Init.** The goal card: *"Ship the parser feature."* One goal, no structure.
2. **Decompose.** The goal becomes a graph, level by level: t1 on top, the t2/t3 fan-out, the t4 join, the t5 tail. Five tasks, five edges, all pending.
3. **Persist.** `save("tasks.json")` — the panel fills with five pending rows. The plan has left the process.
4. **Pick t1.** The only task with no dependencies. It turns purple: running.
5. **Complete t1.** The node flips green and its file row flips `done` in the same beat — mark done, then save, two adjacent lines.
6. **Unlock.** Both edges out of t1 are green: t2 and t3 lift into the ready frontier together.
7. **Pick t2.** Two tasks are legal; the tie-breaker takes the lower id.
8. **Complete t2.** File now reads t1 ✓ t2 ✓. And note the status line: t4 is *still blocked* — one green edge into a join isn't enough.
9. **Pick t3.** The tests start.
10. **Crash.** The process is killed while t3 runs. Everything in memory — the graph, the header readout — collapses under a red *process killed* stamp. The file panel doesn't flinch: it still reads t1 ✓, t2 ✓, t3 *pending*. The dying screen said t3 was running; the file never heard about it. Completion, not intention, is what gets persisted.
11. **Restart.** A new process. Its first act is `load("tasks.json")` — and the graph redraws *from the file*: t1 and t2 come back already green. Nothing was re-derived, re-planned, or re-executed.
12. **Resume.** `next_ready` recomputes the frontier from the loaded state and hands back t3. Zero completed tasks re-run; the in-flight one restarts, because the file (correctly) never recorded it as done.
13. **Complete t3, unlock t4.** The join finally has both its edges.
14. **Pick t4, complete t4, unlock t5. Pick t5, complete t5.** The rest of the run is ordinary: pick, complete, save, unlock.
15. **Done.** The ledger: 5 tasks, 5 completed executions, plus 1 aborted attempt. The crash cost exactly the task it interrupted.

Now flip **store** to `in-memory` and keep the crash. Same graph, same tie-breaks, same crash point — but the restart finds nothing to load. The graph is re-decomposed with all five tasks pending, and the loop grinds through t1 and t2 *again*, each wearing a red "again" chip. Final ledger: 7 completed executions for 5 tasks. The two extra are the completed work that existed only in a dead process's memory. And run either store with **scenario** set to `smooth` for the honest control: no crash, and the two stores run the same tasks in the same order at the same cost. Persistence is invisible until the day it's everything. Finally, sweep **crash after** from 0 to 4: on disk the bill stays flat — whatever dies, the restart re-runs exactly one in-flight task — while in memory it climbs by one execution for every task the crash erases, from nothing lost at 0 (nothing was done yet) to four tasks paid for twice at 4. That line, flat versus climbing, is the whole argument for the file.

## Why Save Inside the Loop

The tempting shortcut is to save once — the graph is right there after `decompose`, write it and move on. Then the crash comes, the restart loads a file that says five pending tasks, and the agent redoes everything. The *plan* survived; the *progress* didn't. Persisting a task graph means persisting its statuses, which is why the second `save` call sits immediately after `task.status = "done"` — the file must never lag reality by more than the one task currently in flight.

The same reasoning explains what's *not* saved: this simple restart policy does not need a persisted `running` marker. Production systems can persist it together with ownership, leases, and recovery rules. If the file said running and the process died, the restart couldn't distinguish "died mid-task" from "task is somehow still going." By writing only completions, the file stays trustworthy at the price of re-running one interrupted task — the cheapest insurance in the whole design.

One more thing falls out for free: a scheduled job can add a timestamp to its readiness conditions, alongside any dependency edges. Recurrence, overlap, and missed runs need additional policy. `next_ready` checks "deps all done"; a cron-style system checks "time has come." Same graph, same loop, same file.

The judgment call runs the other way too. When every task is seconds long and free to re-run, the file buys nothing worth its bookkeeping — persistence pays in proportion to what a crash would erase, which is why your shell scripts don't keep a `tasks.json` and your data pipelines do.

## Edge Cases

- **The crash lands between a completion and its save.** Mark-done and save are two adjacent lines, not an atomic unit, so the window is tiny but real: die exactly there and the restart re-runs one *completed* task. Atomic writes (write-to-temp, then rename) protect checkpoint-file integrity; they do not atomically commit an external task effect with its completion record. That gap requires idempotency, transactional coordination, or an explicit recovery protocol. A journal helps only within the guarantees of its design.
- **Two tasks ready, one worker.** After t1, both t2 and t3 are legal. The graph doesn't order them — any tie-breaker is correct, and a multi-worker harness could take both. What the graph *does* guarantee is that t4 waits for both, whoever finishes last.
- **A cycle in the dependencies.** t2 needs t3 and t3 needs t2: `next_ready` returns nothing, forever, with every task behind the cycle pending. A real decomposer validates the graph is acyclic before saving it.
- **The goal changes mid-run.** The file describes the old decomposition. A restart resumes the old plan faithfully — durability cuts both ways. Re-planning requires deliberately deleting or versioning the file.

## Common Mistakes

- **Persisting the plan but not the progress.** Saving once after `decompose` feels done and isn't — the restart loads a pristine plan with zero completions and redoes the run. Save belongs inside the loop.
- **Trusting the screen over the file.** At crash time the screen said t3 was running; the file said pending. The file is right: "running" is an intention, and intentions don't survive processes. Resume logic should read persisted state only.
- **Treating the order as the agent's job.** If the loop "knows" to run tests before the suite, that knowledge dies with the loop. Encode it as an edge (`t4.deps = [t2, t3]`) and the ordering survives restarts, re-plans, and other workers, because it's data.
- **Marking a task done when it was merely started.** The whole resume guarantee rests on `done` meaning *finished*. Persist optimistically and a crash leaves a half-executed task the restart will skip forever.
- **Rebuilding the graph on every start.** `load` before `decompose` isn't an optimization, it's the point — a task system that re-plans on restart is an in-memory system with extra steps.

## A Note on Simplification

The task-system mechanics on screen — decomposition into a DAG, dependency gating, the save-per-completion discipline, the crash, the resume — are real and deterministic; that's what this lesson exists to teach. The file is also simulated by a separate in-memory checkpoint, not written to your disk. The task names and their "execution" are scripted for clarity: nothing actually scaffolds a module, and the decomposition itself would, in a real agent, come from a model call — [tool-calling](https://vizlearn.app/ai/agents/tool-calling) and [react-loop](https://vizlearn.app/ai/agents/react-loop) show recorded runs of that machinery. That last point cuts into the memory-mode story too: this demo's restart re-plans the *identical* graph because its `decompose` is deterministic, while a real model's re-plan may produce a different decomposition entirely — so the fixed redo count here does not bound a real model’s recovery cost. Real task systems add what this one omits: atomic file writes and journaling (the completion-save gap above), retry policies and per-task timeouts, priorities beyond lowest-id, parallel workers claiming tasks from the same file, and cycle validation at decompose time. And a naming note, because you already use this pattern: this loop is the core of a workflow engine (Airflow, Temporal), of a CI pipeline resuming a half-finished build, and of the todo file an agent harness keeps on disk. None of that changes the shape: put the plan and its progress on disk, gate execution on the edges, and a process becomes something you can afford to lose.
