> ## Documentation Index
> Fetch the complete documentation index at: https://taskgraph-ai.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Dependency Graph

> How TaskGraph picks the next task, finds bottlenecks, the critical path and parallel waves, explains blocked tasks, and records where each dependency came from

Task dependencies form a graph. TaskGraph reads that graph to pick the next task, to show what holds up the most work, and to explain why a task can't start. The analysis (bottlenecks, critical path, parallel waves, blockers, cycles) lives in `@tm/core` (`packages/tm-core/src/modules/dependencies/`), and `taskgraph graph`, `taskgraph why`, `taskgraph next` and the `dependency_analysis` MCP tool only display it. Some older code paths still do their own thing; see [Open issues](#open-issues).

## How `next` orders tasks

`taskgraph next` (and the recommendation in `taskgraph list`) considers tasks that are `pending` or `in-progress` and whose dependencies are all `done`. Subtasks of an in-progress parent are considered first, when both their own dependencies and their parent's are done (see [Parents and subtasks](#parents-and-subtasks)). A subtask whose parent is not `in-progress` is not offered on its own: `next` offers the parent, and once you start it, `next` offers its subtasks. Among the candidates it picks by:

1. **Priority**: `critical`, then `high`, `medium`, `low`.
2. **Unblocks the most**: the task with the most unfinished tasks waiting on it, directly or through other tasks. Between two subtasks that tie, the one more of its own task waits on.
3. **Fewer dependencies**: the task that lists fewer dependencies.
4. **ID**: the lowest ID.

Step 2 counts every unfinished task, including `deferred`, `blocked` and `review` ones, because they still wait on their dependencies. A task is "unfinished" unless it is `done` or `cancelled`.

It counts top-level tasks: a parent and its subtasks are one piece of work, however finely the task was expanded. A task in front of a parent with ten subtasks counts that parent once, the same as before the parent was expanded, and subtasks that wait through their parent count toward that parent. A subtask doesn't count its own parent or siblings, so one that holds up only its own task counts 0 (step 2 then compares how many of its siblings wait on it).

## How a subtask's dependencies are read

A subtask's `dependencies` can name a sibling, another task's subtask or a top-level task. The stored value decides which:

* **A number below 100** (`3`) is a sibling subtask: `3` under task 2 is `2.3`.
* **A dotted string** (`"4.1"`) is that subtask.
* **Anything else** (a string without a dot, like `"3"`, or a number of 100 or more) is a top-level task.

`add-dependency` and `add-subtask --dependencies` (and their MCP tools) take the IDs you see in `list`: `3` is task 3 and `2.1` is that subtask. They store a task as a string (`"3"`), so it is never read as a sibling. To depend on a sibling, give its dotted ID (`2.1` under task 2). `add-subtask` rejects an entry that is not a task or subtask ID.

When `expand`, `update-task` or `update` has the AI write subtasks, the AI uses the same forms: a number for an earlier subtask in its list, a string for a task (`"3"`) or another task's subtask (`"4.1"`). Entries that are none of these, and a dependency on the subtask's own parent, are dropped. `update-task` also drops a sibling number that is not earlier than the subtask itself.

On a top-level task there is no sibling to name: a number or a plain string is a task, and a dotted string is a subtask. `add-dependency` and `add-task --dependencies` store a task there as `"3"` and a subtask as `"3.2"`, the form tm-core saves, and `add-dependency` rewrites a list that mixes `3` and `"3"` into that form when it adds to it. `next`, `why`, `graph`, the dependency commands and the MCP tools all read dependencies by this rule (`resolveSubtaskDependency` in `@tm/core`).

## Parents and subtasks

A subtask is part of its parent, so the analysis links them both ways:

* **A subtask waits on what its parent waits on.** If task 3 depends on task 2, subtask 3.1 can't start before task 2 is done either, even though 3.1 lists nothing. `why`, `next`, the critical path, bottleneck counts and loop checks all follow this. A parent's dependency on one of its own subtasks is not passed down.
* **A parent isn't finished until its subtasks are.** The critical path orders a parent after its open subtasks, so a chain can run `1 → 3.1 → 3.2 → 3 → 5`. A subtask that lists its own parent keeps that dependency, and the parent is then not ordered after it.

`validate-dependencies` and `fix-dependencies` check only the dependencies written on each task and subtask, so they don't report a loop that only exists through a parent (task 2 needs 4, and 4 needs 2.1). `why` and the `cycles` of `dependency_analysis` do.

## `taskgraph graph`

```bash theme={null}
taskgraph graph                  # all tasks as a tree
taskgraph graph --id=3           # one task and the tasks drawn under it
taskgraph graph --format json    # machine-readable
```

Each task is drawn once, under the dependency it waits on longest. Its other dependencies follow the title (`needs 4`). The task `next` would pick is marked `← next`; when `next` picks a subtask, its parent is marked `← next: 3.2`. Tasks in a dependency loop and dependency IDs that match no task are called out.

```
○ 1  Set up the database · wave 1 · blocks 3 ◆  ← next
└─ ○ 2*  Build the login API · wave 2 · blocks 2 ◆
   └─ ○ 3  Add the dashboard · wave 3 · needs 4* · blocks 1 ◆
      └─ ○ 5  Write docs · wave 4 ◆

○ 4  Design the schema · wave 1 · blocks 2

Bottlenecks
  1  blocks 3  Set up the database
  2  blocks 2  Build the login API
  4  blocks 2  Design the schema
  3  blocks 1  Add the dashboard

Critical path: 1 → 2 → 3 → 5
4 waves · 2 tasks can run now: 1, 4

○ pending
◆ critical path  * dependency inferred by AI
```

* **`blocks N`** marks a bottleneck: an unfinished task that N unfinished top-level tasks wait on, directly, through other tasks, or through a subtask. A parent and its subtasks count as one, as in [`next`](#how-next-orders-tasks). The list shows the top five, most first, ties to the lowest ID. Tasks that block nothing are left out.
* **`wave N`** is the task's parallel wave. Wave 1 is every task that can run now: `pending` or `in-progress` with all its dependencies finished. Wave N holds the tasks whose dependencies are all finished or in earlier waves, so everything in one wave can run side by side. The line under the critical path counts the waves and names wave 1; `N can't be scheduled` counts the unfinished tasks with no wave (see [Parallel waves](#parallel-waves)).
* **`◆`** marks the critical path: the longest chain of unfinished work, dependencies first, through parents and subtasks as [described above](#parents-and-subtasks). Ties go to the lowest IDs.
* **`needs 2.1`**: a dependency on a subtask places the task under the subtask's parent (task 2), and the subtask is always listed after the title, since the parent is not what the task waits on.
* **`*`** marks a dependency the AI chose rather than you (see [Dependency provenance](#dependency-provenance)). After an ID in a `needs` list, it marks that dependency. After a task's own ID (`2*`), it marks that task's edge to the task drawn above it.

### JSON output

`--format json` returns the tree plus the analysis:

| Field | Contents |
| - | - |
| `roots`, `cyclic`, `total` | The tree. Each node has `id`, `title`, `status`, `priority`, `alsoNeeds`, `missing`, `children` |
| `roots[].inferred` | On every node: the dependency IDs (the tree parent included) recorded as AI-inferred. Anything not listed is explicit |
| `bottlenecks` | Up to five `{ id, title, status, blocks }`, most first |
| `criticalPath` | Task IDs, dependencies first |
| `roots[].wave` | On every node: its parallel wave (1 = can run now), or `null` when finished or unschedulable |
| `waves` | `{ waves, waveOf, unscheduled }`: task IDs per wave (`waves[0]` is wave 1), each scheduled task's wave by ID, and `{ id, reason, blockers }` for every unfinished task with no wave |
| `tag`, `nextId` | The tag shown and the task `next` would pick |

### Known limits

* Tasks in a dependency cycle, and tasks that depend on one, are left out of the critical path, since they can't be started. Use `taskgraph why` to find and break the loop.
* The tree shows top-level tasks only. `bottlenecks` and `criticalPath` can name subtasks such as `2.1`; they appear in those lists but not as tree nodes.
* The tree reads only top-level tasks' dependencies, so a loop formed by a subtask's own dependencies (task 4 needs 2.1, and 2.1 needs 4) is not called out in the tree; `why`, `validate-dependencies` and the `cycles` of `dependency_analysis` report it. A loop through a parent's dependencies (task 2 needs 4, and 4 needs 2.1) is called out.
* A loop that only exists because a parent finishes after its subtasks (subtask 2.1 needs task 3, and 3 needs task 2) keeps those tasks off the critical path, but is not reported as a cycle: each task can still start.
* A dependency on an archived task (see `taskgraph archive`) counts as done and isn't drawn: the task sits at the top level, or under its other dependencies.
* `--id` draws the task and the tasks placed under it. Each task is drawn under only one dependency (the one it waits on longest), so a task that also needs the chosen task, but is drawn under another dependency, does not appear. The chosen task's own parent edge is not shown or marked, and bottlenecks and the critical path still cover the whole tag.

### Parallel waves

`parallelWaves(tasks)` in `@tm/core` (`parallel-waves.ts`) groups unfinished tasks into waves. It is what `graph` shows and what a runner that starts several tasks at once should read.

* **Top-level tasks only.** A task is the unit of parallel work; its subtasks run inside it. This matches the tree `graph` draws.
* **Finished tasks** (`done`, `cancelled`) get no wave and satisfy whatever waits on them.
* **A dependency on a subtask** (`2.1`) is satisfied once that subtask or its parent is finished. Until then the task goes in a wave after the parent (task 2).
* **Unfinished tasks with no wave** are listed in `unscheduled` with a `reason`:
  * `cycle`: in a dependency loop (a task that lists itself counts). `blockers` names the loop members it depends on.
  * `missing`: names a dependency ID that matches no task. `next` never picks such a task, so it gets no wave either. `blockers` lists the IDs.
  * `held`: `review`, `blocked` or `deferred`. Only `pending` and `in-progress` tasks go in a wave.
  * `waiting`: depends on a task that has no wave. `blockers` lists those tasks.

## `taskgraph why`

```bash theme={null}
taskgraph why 5            # why can't task 5 start?
taskgraph why 3.2          # subtasks work too
taskgraph why 5 --format json
```

`why` follows a task's unfinished dependencies down to the root causes and prints the tree of blockers, then what to do:

```
○ 5  Write docs
Blocked by:
└─ ○ 3  Add the dashboard
   ├─ ○ 2  Build the login API
   │  └─ ○ 1  Set up the database  ← ready to start
   └─ ○ 4  Design the schema  ← ready to start

To unblock:
  Do 1 (Set up the database) and 4 (Design the schema): nothing else stands
    in their way.
```

Each step wraps to the terminal width, continuing under a deeper indent.

A chain ends at one of four root causes: `ready` (unfinished, nothing in its way), `stalled` (`deferred` or `blocked`), `missing` (the ID matches no task) or `cycle` (it depends on itself through the chain). The JSON has `id`, `title`, `status`, `blocked`, `tag`, a nested `blockers` tree and `rootCauses`; see [`why`](https://taskgraph-ai.com/capabilities/cli-viewing-commands#explain-why-a-task-is-blocked) for the full shape.

A subtask also waits on what its parent waits on. `why` draws those blockers under the parent, marked `← parent`:

```
○ 3.1  Write the chart queries
Blocked by:
└─ ○ 3  Add the dashboard  ← parent
   ├─ ○ 2  Build the login API
   │  └─ ○ 1  Set up the database  ← ready to start
   └─ ○ 4  Design the schema  ← ready to start

To unblock:
  Do 1 (Set up the database) and 4 (Design the schema): nothing else stands in their way.
```

In the JSON, that node has `parentOf` set to the subtask's ID. The parent itself is not something to finish first; only its blockers are.

### Exit codes

`why` and `graph` exit 0 on success, whether or not the task is blocked. They exit 1 for a missing or malformed ID (`why`), an unknown task or tag, or an unsupported `--format`.

## Dependency provenance

Every dependency is either **explicit** (you added it, or it was already there) or **inferred** (the AI chose it in `parse-prd`, `add-task`, `expand`, `update`, `update-task`, or `scope-up` / `scope-down`). A dependency you asked for in an update prompt is still recorded as inferred, because the model decides which IDs to write. The other AI commands (`research`, `analyze-complexity`, `update-subtask`) never write dependencies.

### Where it is stored

Provenance lives in the task's `metadata.dependencySources`, a map from dependency ID to `"inferred"`:

```json theme={null}
{
  "id": "3",
  "dependencies": ["2", "4"],
  "metadata": {
    "dependencySources": { "4": "inferred" }
  }
}
```

* **Only inferred dependencies are stored.** A dependency with no entry is explicit, so existing files need no migration.
* **Keys are full IDs**: the ID the dependency names by the [rule above](#how-a-subtasks-dependencies-are-read). A task's keys are plain IDs (`"4"`). On a subtask of task 2, a sibling is keyed `"2.1"` whether it is stored as `1` or `"2.1"`, and a task stored as `"3"` is keyed `"3"`. Code reads marks through `getDependencySource` in `@tm/core`, never from raw keys.
* **`dependencySources` is a reserved metadata key.** The `metadata` parameter of the MCP `update_task` and `update_subtask` tools (available only with `TASKGRAPH_ALLOW_METADATA_UPDATES=true`) drops it, so a hand-written value can't overwrite provenance.
* Marks for a dependency that is removed are dropped with it, and a moved task carries its marks to the new ID.
* Running `add-dependency` for a dependency that is already inferred makes it explicit.
* Known limit: when `update-task` regenerates subtasks and the AI reorders them, subtasks are renumbered without remapping their sibling dependencies, so a sibling dependency can change source.

## Links that don't block

A dependency means "wait for this task". Sometimes two tasks are only connected: they touch the same code, or you found one while doing the other. Link them without making either wait:

```bash theme={null}
# Task 7 is related to task 4
taskgraph add-dependency --id=7 --depends-on=4 --type=related

# File work you found while doing task 3
taskgraph add-task --prompt="Fix the flaky login test" --discovered-from=3
```

There are three link types:

| Type | Blocks? | Set with |
| - | - | - |
| `blocks` | Yes | `add-dependency` (the default), `add-task --dependencies` |
| `related` | No | `add-dependency --type=related` |
| `discovered-from` | No | `add-dependency --type=discovered-from`, `add-task --discovered-from` |

Only `blocks` counts anywhere a task waits: `next`, `why`, `graph` (blocked counts and the critical path), loop checks, and `validate-dependencies` / `fix-dependencies`. `show` lists the other two after what the task depends on:

```
Task 7  Export bookmarks
○ pending · medium priority · depends on 2 · related to 4 · discovered from 3
```

* **Changing the type.** Running `add-dependency --type=related` on a task that already depends on the other turns the dependency into a link, so the task stops waiting. Running plain `add-dependency` on a linked pair turns the link back into a dependency.
* **Removing a link.** `remove-dependency` removes a link the same way it removes a dependency.
* **Cleanup.** Removing a task removes the links to it (and to its subtasks), as it does dependencies. Moving a task within a tag carries the links to its new ID. Moving it to another tag drops links that would cross tags. `validate-dependencies` reports a link to a task that doesn't exist and `fix-dependencies` removes it.
* **MCP.** `add_dependency` takes `type` (`blocks`, `related` or `discovered-from`) and `add_task` takes `discoveredFrom`.

### Where links are stored

Non-blocking links live in the task's `metadata.taskLinks`, a map from full ID to link type, not in `dependencies`:

```json theme={null}
{
  "id": "7",
  "dependencies": ["2"],
  "metadata": {
    "taskLinks": { "4": "related", "3": "discovered-from" }
  }
}
```

Everything in `dependencies` is a `blocks` link, so files written before link types existed load unchanged. Keeping the other links out of `dependencies` means no code that reads dependencies can mistake them for blockers. Keys are always full IDs (`"2.1"` for a subtask), on a task or a subtask. If the same task is both a dependency and a link, the dependency wins. Like `dependencySources`, `taskLinks` is a reserved metadata key that the `metadata` parameter of `update_task` and `update_subtask` drops.

## Schema version

Each tag in `.taskgraph/tasks/tasks.json` records `metadata.schemaVersion` (currently `1`), stamped whenever TaskGraph saves the tag, from any command, and in the empty file `init` creates. A missing or malformed value is read as version 1. A tag written by a newer version, or with a malformed `schemaVersion`, prints a warning to stderr once per file and tag, and is still read. The stamp never lowers a version already on disk.

## MCP

The `dependency_analysis` tool, in the `standard` and `all` tiers, returns the bottlenecks, critical path and cycles for a tag. Pass `id` to also get an `explanation` with the blockers and root causes for one task (what `taskgraph why` shows). `limit` sets how many bottlenecks come back (default 5). A `tag` the project doesn't have is an error, not an empty result. See [MCP tool loading](https://taskgraph-ai.com/capabilities/mcp.md) for the tiers.

## Open issues

These are known and not yet fixed:

* **What counts as finished differs for subtasks.** `next`, `why` and `graph` all treat `done` and `cancelled` as finished, but `next` only looks for finished work under parents that are `pending`, `in-progress`, `done` or `cancelled`. A `done` subtask under a parent in `review`, `blocked` or `deferred` satisfies a dependency in `why` and `graph`, but never in `next`.
* **Older writers don't stamp a schema version.** Tags created by legacy code paths have no `schemaVersion` until the next `@tm/core` save; a missing value means version 1.
* **Graph limits:** cycle members and tasks behind a loop are left out of the critical path, and the tree doesn't call out a loop formed only by a subtask's own dependencies (see [`taskgraph graph`](#known-limits)).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.