Understanding turbo's "topo" task

Why do the turborepo docs suggest a “topo” task instead of each task depending on it’s upstream dependencies directly?

For example, why is the first example better than the second?

{
  "tasks": {
    "topo": {
      "dependsOn": ["^topo"]
    },

    "type": {
      "dependsOn": ["dag"]
    }
  }
}
{
  "tasks": {
    "type": {
      "dependsOn": ["^type"]
    }
  }
}

In practice, my type/lint/test tasks all depend on “^build”, but I seem to have the same result as “topo”. I’m just trying to understand the logic, as it remains unexplained in the docs.

This is explained in the docs here: Configuring tasks | Turborepo

1 Like

Thanks. I’ve ready that section multiple times. I don’t see why “transit” is necessary.

Shouldn’t turbo be able to detect what tasks can be parallel based on the individual task’s inputs? If the inputs of an upstream task have changed, preserve the dependency. The way I read the docs, it suggests that changes are detected at the package level—which I can’t take to be correct.

What am I missing?

Perhaps a more realistic example? How can this graph be improved, given the normal inputs are “build”/“type”? Does “topo” actually do anything? What if there were packages that didn’t require a build step?

    "topo": {
      "dependsOn": ["^topo"]
    },

    "ready": { // prevals, data fetches, etc.
      "dependsOn": ["topo", "^build"]
    },

    "build": {
      "dependsOn": ["topo", "^build", "ready"]
    },

    "type": { // tsc --noEmit
      "dependsOn": ["topo", "^build", "ready"]
    },

The topo task collapses the dependency graph into one, flat, graph, allowing you to have source code dependency on tasks that you still want to run in parallel. This is helpful when dependent tasks don’t have outputs, but you don’t want ui#type-check to delay the start of web#type-check.

If you don’t have topo, you can change code in ui but still hit cache for web.

In the case you’re showing, topo may or may not do something depending on your package graph. Turborepo will always parallelize everything that it can, so if ^build is the bottleneck, then topo won’t have any effect.

Ok, so “topo” is specifically for a task that does NOT depend on it’s dependencies’ task, but DOES depend on changes to its dependencies.

  • This is always useful for tasks like code formatting, which don’t truly depend on “^format”.
  • It MIGHT be useful for type checking, ONLY IF your shared packages don’t require a build step.
  • It is NEVER useful for build tasks, which should always depend on upstream builds.

So the magic in “topo” is really the default turbo “inputs” (basically everything). You can conceivably have multiple “topo” tasks. For example, “topo:typescript” would be useful to narrow cache invalidation to *.ts and tsconfig.json.


Do I have this all correct?

1 Like

That all sounds right, yes!

1 Like

Thanks for your help!

I’d just like to suggest you update the docs to go deeper into the strategy. I can’t be the only one who didn’t grasp this. (And if I am, well… :sweat_smile:)

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.