moonrepo: how do I run tasks in dependency order and still parallelize safely?
Developer Productivity Tooling

moonrepo: how do I run tasks in dependency order and still parallelize safely?

10 min read

Many teams adopting moonrepo quickly ask the same question: how can you respect dependency order while still getting maximum safe parallelism? You want tasks to run only after their dependencies are completed, but you also want to avoid a slow, strictly sequential pipeline.

This guide explains exactly how moonrepo handles dependency ordering and parallel execution, and shows you the patterns, configs, and commands you’ll use to run tasks in dependency order and still parallelize safely.


How moonrepo thinks about task ordering and parallelism

moonrepo provides two key concepts that make safe parallelization possible:

  1. Task graph – a directed acyclic graph (DAG) of all tasks and their dependencies.
  2. Runner – the engine that walks this graph, respecting dependencies and running tasks in parallel when possible.

When you run commands like:

moon run :test
# or
moon run project:test
# or
moon run :build

moon builds a graph of everything that needs to run and then:

  • Ensures no task starts before its required inputs are ready (dependencies first).
  • Starts as many independent tasks in parallel as your configuration allows.

The result: dependency order is enforced per edge in the graph, while siblings (tasks without direct dependency relationships) can run in parallel.


Defining dependencies between tasks

To parallelize safely, moon needs to know the correct order. That comes from:

  • Project dependencies (e.g., packages and libraries that depend on each other).
  • Task dependencies (a specific task must finish before another can start).

1. Project dependencies

In monorepos, project dependency graphs are often inferred from:

  • package.json dependency relationships in JavaScript/TypeScript repos.
  • Workspace manifests like pnpm-workspace.yaml, yarn.lock, package-lock.json, etc.
  • Other language ecosystem manifests (e.g., Rust’s Cargo.toml, etc., depending on your setup).

moon reads these and understands which projects depend on which. For example:

  • app depends on ui
  • ui depends on core

When you run moon run :build from the workspace root, moon will:

  • Build core first
  • Then ui (after core is done)
  • Then app (after ui is done)

But all independent branches can run at the same time. If api depends on core but not on ui, api and ui builds can run in parallel once core is built.

2. Task dependencies (dependsOn)

You can express finer-grained ordering inside moon.yml or language-specific configs by setting dependsOn for tasks.

Example moon.yml for a project:

tasks:
  lint:
    command: pnpm lint
  test:
    command: pnpm test
    dependsOn:
      - lint
  build:
    command: pnpm build
    dependsOn:
      - test

In this example:

  • linttestbuild is a chain.
  • For a single project, they’ll run in that order.
  • Across multiple projects, moon will respect both:
    • The project dependency graph
    • The task dependency chain

So if you have several projects with the same tasks, moon will run:

  • All lint tasks that are unblocked
  • Then all test tasks for projects whose lint finished
  • Then all build tasks that depend on test

Within each "layer", tasks are parallelized automatically, as long as they don’t depend on each other directly.


Parallelization: how moon decides what can run at the same time

Once the dependency graph is constructed, moon performs a topological traversal:

  1. Find all tasks with no unmet dependencies.
  2. Run up to concurrency of them in parallel.
  3. As tasks finish, unlock tasks that depended on them.
  4. Repeat until all tasks are done.

This guarantees:

  • No dependency is violated – a task never runs before its dependencies complete successfully.
  • Available parallel work is exploited – if there are 20 independent tasks and your concurrency limit is 8, moon will run 8 at a time.

You control parallelism via:

moon run :test --concurrency 4
# or environment variable
MOON_CONCURRENCY=4 moon run :test

If you don’t specify concurrency, moon chooses a reasonable default (often tied to CPU cores) while still respecting the graph.


Running tasks in dependency order by scope

You often don’t want to run every task in the repo. You want to target the task graph starting from a particular project or set of projects.

Run from a project root

From a project directory:

cd apps/web
moon run build

This:

  • Builds the task graph for web:build.
  • Traverses backwards through its project dependencies (e.g., ui, core, etc.).
  • Runs their relevant tasks (typically defined as inputs for build) in dependency order.
  • Parallelizes across branches that don’t depend on each other.

Run from the workspace root for a specific task

moon run :build

This means "run the build task for every project that has it". The graph:

  • Includes all build tasks.
  • Adds any tasks that build depends on via dependsOn.
  • Uses the project dependency graph to order them.

Again, parallelization happens automatically whenever the graph allows it.


Safe parallelization across dependent projects

Consider a common scenario:

  • libs/core is a shared library
  • libs/ui depends on core
  • apps/web depends on ui
  • apps/admin depends on both ui and core

Each project defines:

tasks:
  build:
    command: pnpm build
    inputs: ["src/**", "package.json"]

When you run:

moon run :build

moon will:

  1. Start with leaf nodes (no dependencies), maybe core if it has none.
  2. After core is built, ui plus any other libs depending on core become runnable.
  3. Once ui and other dependencies are done, web and admin builds are unlocked.

Parallelism example:

  • If core is the only root dependency, it runs alone.
  • When ui and api both depend on core, both ui:build and api:build can run simultaneously after core:build.
  • web:build and admin:build only start once their dependencies’ builds are completed.

All of this is automatic as long as your project dependencies are correct.


Fine-tuning: avoid overserializing and oversharing

Over-serialization and global dependencies can kill parallelism. To parallelize safely and effectively with moonrepo:

Avoid a single massive "prepare everything" step

Instead of:

tasks:
  prepare:
    command: pnpm install && pnpm lint && pnpm test && pnpm build

  deploy:
    command: pnpm deploy
    dependsOn:
      - prepare

Break it into smaller tasks with targeted dependencies:

tasks:
  install:
    command: pnpm install
  lint:
    command: pnpm lint
    dependsOn:
      - install
  test:
    command: pnpm test
    dependsOn:
      - install
  build:
    command: pnpm build
    dependsOn:
      - test
  deploy:
    command: pnpm deploy
    dependsOn:
      - build

Now:

  • lint and test can run in parallel after install.
  • build waits only on test, not lint.
  • The runner has more flexibility to keep CPUs busy.

Don’t overuse global ‟dependsOn”

If you say "build" dependsOn "test" everywhere, you may create unnecessary chains.

Use task dependencies only where required:

  • In libs, maybe build doesn’t need test to pass (you run tests at a higher level).
  • In apps, you might require test before build.

Tailor dependsOn to how you actually ship and verify each project.


Using --affected to reduce work while keeping safe order

For CI and large monorepos, you usually want:

  • Only tasks affected by recent changes
  • Still run in dependency order
  • Still parallelized safely

moon run :test --affected (or the equivalent in your CI scripts) will:

  1. Determine which projects are affected by changes (e.g., based on Git diff).
  2. Build the dependency-aware task graph only for those projects and their upstream dependencies.
  3. Run them in parallel where safe.

This is essential for keeping big pipelines fast while maintaining correctness.


Controlling concurrency and resource pressure

moon allows you to tune parallelism to avoid overloading CI nodes or local machines.

Set concurrency explicitly

moon run :build --concurrency 6

This caps maximum parallel tasks at 6. moon still respects dependency order; it just won’t exceed this concurrency level.

Task-level resource hints (pattern)

If you have very heavy tasks (e.g., large builds) and light ones (e.g., lint), you can structure your pipeline so that:

  • Heavy tasks are fewer and maybe grouped.
  • Light tasks run alongside heavy tasks to fully utilize CPU.

While moon does not (as of this writing) do deep resource scheduling, careful task design and dependsOn usage give the runner more freedom to intermix heavy and light work in parallel.


Caching: a key part of safe, fast parallelism

moonrepo’s caching plays a big role in how tasks run:

  • If a task’s inputs are unchanged and a cached result exists, the task is restored from cache, not rerun.
  • Every task still respects dependency order: a cached dependency is treated as "already completed".
  • Parallelism is between cache misses; cache hits are effectively instantaneous.

This means:

  • Re-running moon run :build after a small change only rebuilds affected parts.
  • Unchanged branches of the graph are skipped quickly through cache.
  • Overall pipeline time drops sharply while staying correct.

Common patterns for safe parallel pipelines

Here are some practical patterns you can use to run tasks in dependency order and parallelize safely with moonrepo:

Pattern 1: Lint → Test → Build, parallel across projects

Per project:

tasks:
  lint:
    command: pnpm lint
  test:
    command: pnpm test
    dependsOn:
      - lint
  build:
    command: pnpm build
    dependsOn:
      - test

Run:

moon run :build

Behavior:

  • All lint tasks run in parallel where dependencies allow.
  • Completed lint tasks unlock test tasks per project.
  • build starts per project after test completes for that project.
  • Project dependency graph ensures libs build before apps.

Pattern 2: Build libs before apps, but allow parallel app builds

Project moon.yml or workspace config ensures dependency tree like:

  • libs/* have no app dependencies.
  • apps/* depend on relevant libs/*.

Then running:

moon run :build

gives:

  • All lib builds in parallel (subject to concurrency and any lib-to-lib deps).
  • When libs are done, all app builds that depend on them can start together.

Pattern 3: CI pipeline with affected tasks

In CI:

moon run :lint --affected --concurrency 8
moon run :test --affected --concurrency 8
moon run :build --affected --concurrency 6

Or a single combined run step, depending on how you structure tasks.

Either way, moon ensures:

  • Only affected projects/tasks are considered.
  • Dependencies are still enforced.
  • Parallelism is used as much as the graph and your concurrency allow.

Troubleshooting: when tasks don’t parallelize as expected

If you think tasks should run in parallel but don’t, check:

  1. Hidden dependencies

    • Are you relying on side effects (e.g., writing to a shared folder) that require implicit ordering?
    • If yes, model that as an explicit dependsOn relationship or refactor the shared step.
  2. Overly broad dependsOn

    • Did you chain tasks unnecessarily (e.g., build depends on lint in libraries that don’t need that guarantee)?
    • Remove or narrow dependsOn to unlock parallel execution.
  3. Project dependency graph

    • Are project dependencies correctly declared (e.g., correct dependencies fields in package.json)?
    • Incorrect or missing dependencies can cause either invalid parallelization or overserialization.
  4. Concurrency limit

    • Is MOON_CONCURRENCY or --concurrency set too low?
    • Increase it until you reach a good balance of speed and resource usage.
  5. Caching expectations

    • If tasks seem to “run” but finish instantly, they may be pulling from cache, which is expected.
    • Use moon’s logs or flags to inspect whether a task was executed or restored from cache.

Summary: how to run moonrepo tasks in dependency order and still parallelize safely

To get the behavior you want:

  1. Model dependencies explicitly

    • Ensure project dependency graph is correct (e.g., via package.json).
    • Use dependsOn on tasks only where required.
  2. Let moon build and walk the task graph

    • Use commands like moon run :build or moon run project:task.
    • Rely on the built-in DAG scheduling to enforce order and parallelize safely.
  3. Tune concurrency and scoping

    • Use --concurrency or MOON_CONCURRENCY to control parallelism.
    • Use --affected and project targeting to limit work while preserving dependency correctness.
  4. Leverage caching and avoid monolithic tasks

    • Smaller, well-defined tasks with proper dependencies give moon more room to parallelize.
    • Caching keeps repeated work fast while still respecting ordering.

With these practices, moonrepo will execute tasks in correct dependency order and still make full use of safe parallelism across your monorepo.