Skip to main content
Unlike other task runners that require the same tasks to be repeatedly defined for every project, moon uses an inheritance model where tasks can be defined once at the workspace-level, and are then inherited by many or all projects. Workspace-level tasks (also known as global tasks) are defined in .moon/tasks/**/*, and are inherited based on conditions. However, projects are able to include, exclude, or rename inherited tasks using the workspace.inheritedTasks in moon.yml.

Overview

Task inheritance enables:
  • DRY principle - Define common tasks once
  • Consistency - Same task configuration across projects
  • Flexibility - Override or extend tasks per-project
  • Maintenance - Update tasks in one place

Conditional Inheritance

Task inheritance is powered by the inheritedBy setting in global task configurations (those in .moon/tasks/**/*). This setting defines conditions that a project must meet in order for inheritance to occur. If the setting is not defined, or no conditions are defined, the configuration is inherited by all projects. The following conditions are supported:
  • file, files - Inherit for projects that contain specific files (does not support globs)
  • language, languages - Inherit for projects that belong to specific languages
  • layer, layers - Inherit for projects that belong to specific layers
  • stack, stacks - Inherit for projects that belong to specific stacks
  • tag, tags - Inherit for projects that have specific tags
  • toolchain, toolchains - Inherit for projects that belong to specific toolchains

Single Condition

One or many conditions can be defined, and all conditions must be met for inheritance to occur. For example, the following configuration will only be inherited by Node.js frontend libraries:
.moon/tasks/node-frontend-library.yml
inheritedBy:
  toolchain: 'node'
  stack: 'frontend'
  layer: 'library'

tasks:
  build:
    command: 'tsup'
    outputs:
      - 'dist'

Multiple Conditions

Each condition supports a single value or an array of values. For example, the previous example could be rewritten to inherit for both Node.js or Deno frontend libraries:
.moon/tasks/js-frontend-library.yml
inheritedBy:
  toolchains: ['node', 'deno']
  stack: 'frontend'
  layer: 'library'

tasks:
  build:
    command: 'build-tool'
    outputs:
      - 'dist'

Clauses

The tags and toolchains conditions support special clauses and, or (the default), and not for matching more complex scenarios.
.moon/tasks/conditional.yml
inheritedBy:
  toolchains:
    or: ['javascript', 'typescript']
    not: ['ruby']
  tags:
    and: ['frontend', 'library']
  layer: 'library'

tasks:
  lint:
    command: 'eslint .'

Real-World Examples

Node.js Projects

.moon/tasks/node.yml
$schema: '../cache/schemas/tasks.json'

inheritedBy:
  toolchains: 'node'

fileGroups:
  sources:
    - 'src/**/*'
  tests:
    - 'tests/**/*'

tasks:
  build:
    command: 'packemon build --addFiles --addExports --declaration'
    env:
      NODE_ENV: 'production'
    inputs:
      - '@globs(sources)'
      - 'package.json'
      - 'tsconfig.json'
    outputs:
      - 'dist'
  
  lint:
    command: 'eslint'
    args:
      - '--cache'
      - '--cache-location'
      - './.eslintcache'
      - '--color'
      - '--exit-on-fatal-error'
      - '.'
    inputs:
      - '@globs(sources)'
      - '@globs(tests)'
      - '.eslintrc.js'
  
  test:
    command: 'jest'
    args:
      - '--cache'
      - '--color'
      - '--passWithNoTests'
    inputs:
      - '@globs(sources)'
      - '@globs(tests)'
      - 'jest.config.js'
  
  typecheck:
    command: 'tsc --build'
    inputs:
      - '@globs(sources)'
      - '@globs(tests)'
      - 'tsconfig.json'
All Node.js projects now automatically inherit these tasks!

Frontend vs Backend

.moon/tasks/frontend.yml
inheritedBy:
  stack: 'frontend'
  toolchain: 'node'

tasks:
  build:
    command: 'vite build'
    outputs:
      - 'dist'
  
  dev:
    command: 'vite'
    preset: 'server'
  
  preview:
    command: 'vite preview'
    preset: 'server'
.moon/tasks/backend.yml
inheritedBy:
  stack: 'backend'
  toolchain: 'node'

tasks:
  build:
    command: 'tsc'
    outputs:
      - 'dist'
  
  dev:
    command: 'node --watch src/index.js'
    preset: 'server'
  
  start:
    command: 'node dist/index.js'

Library vs Application

.moon/tasks/library.yml
inheritedBy:
  layer: 'library'

tasks:
  build:
    command: 'tsup'
    inputs:
      - 'src/**/*'
      - 'package.json'
      - 'tsconfig.json'
    outputs:
      - 'dist'
.moon/tasks/application.yml
inheritedBy:
  layer: 'application'

tasks:
  build:
    command: 'webpack'
    deps:
      - '^:build'  # Build all dependencies first
    inputs:
      - 'src/**/*'
      - 'webpack.config.js'
    outputs:
      - 'dist'
  
  deploy:
    command: 'deploy-script'
    deps:
      - '~:build'

Merge Strategies

When a global task and local task of the same name exist, they are merged into a single task. To accomplish this, one of many merge strategies can be used. Merging is applied to the parameters: Using the options: Each of these options support one of the following strategy values:
  • append (default) - Values found in the local task are merged after the values found in the global task
  • prepend - Values found in the local task are merged before the values found in the global task
  • preserve - Preserve the original global task values
  • replace - Values found in the local task entirely replace the values in the global task

Append Strategy

The default strategy. Local values are added after global values:
# Global: .moon/tasks/node.yml
tasks:
  build:
    command: 'webpack'
    args:
      - '--mode'
      - 'production'
    inputs:
      - 'src/**/*'

# Local: packages/app/moon.yml
tasks:
  build:
    args:
      - '--color'
    inputs:
      - 'webpack.config.js'
    options:
      mergeArgs: 'append'      # default
      mergeInputs: 'append'    # default

# Merged result
tasks:
  build:
    command: 'webpack'
    args:
      - '--mode'
      - 'production'
      - '--color'              # Appended
    inputs:
      - 'src/**/*'
      - 'webpack.config.js'    # Appended

Prepend Strategy

Local values are added before global values:
# Global
tasks:
  build:
    command: 'webpack'
    args:
      - 'build'
    deps:
      - 'designSystem:build'

# Local
tasks:
  build:
    args:
      - '--config'
      - 'webpack.prod.js'
    deps:
      - 'utils:build'
    options:
      mergeArgs: 'prepend'
      mergeDeps: 'prepend'

# Merged result
tasks:
  build:
    command: 'webpack'
    args:
      - '--config'
      - 'webpack.prod.js'      # Prepended
      - 'build'
    deps:
      - 'utils:build'          # Prepended
      - 'designSystem:build'

Replace Strategy

Local values completely replace global values:
# Global
tasks:
  build:
    command: 'webpack'
    inputs:
      - 'src/**/*'
      - 'webpack.config.js'
    outputs:
      - 'dist'

# Local
tasks:
  build:
    inputs:
      - 'app/**/*'             # Completely different
    options:
      mergeInputs: 'replace'

# Merged result
tasks:
  build:
    command: 'webpack'
    inputs:
      - 'app/**/*'             # Replaced
    outputs:
      - 'dist'                 # Inherited (not replaced)

Preserve Strategy

Keep only global values, ignore local values:
# Global
tasks:
  build:
    command: 'webpack'
    args:
      - '--mode'
      - 'production'

# Local
tasks:
  build:
    args:
      - '--mode'
      - 'development'          # Will be ignored
    options:
      mergeArgs: 'preserve'

# Merged result
tasks:
  build:
    command: 'webpack'
    args:
      - '--mode'
      - 'production'           # Preserved from global

Complete Example

All strategies demonstrated together:
# Global: .moon/tasks/node.yml
tasks:
  build:
    command: 'webpack'
    args:
      - '--mode'
      - 'production'
      - '--color'
    deps:
      - 'designSystem:build'
    inputs:
      - 'src/**/*'
      - 'webpack.config.js'
    outputs:
      - 'dist'

# Local: packages/app/moon.yml
tasks:
  build:
    args:
      - '--no-color'
      - '--no-stats'
    deps:
      - 'utils:build'
    inputs:
      - 'app.config.js'
    options:
      mergeArgs: 'append'      # Add after global args
      mergeDeps: 'prepend'     # Add before global deps
      mergeInputs: 'replace'   # Replace global inputs

# Merged result
tasks:
  build:
    command: 'webpack'
    args:
      - '--mode'
      - 'production'
      - '--color'
      - '--no-color'           # Appended (note the conflict!)
      - '--no-stats'
    deps:
      - 'utils:build'          # Prepended
      - 'designSystem:build'
    inputs:
      - 'app.config.js'        # Replaced
    outputs:
      - 'dist'                 # Inherited unchanged

Controlling Inheritance

Projects can control which tasks they inherit:

Include Specific Tasks

packages/special/moon.yml
workspace:
  inheritedTasks:
    include:
      - 'lint'
      - 'test'
      - 'typecheck'
    # Only these tasks are inherited, all others ignored

Exclude Specific Tasks

packages/custom/moon.yml
workspace:
  inheritedTasks:
    exclude:
      - 'build'        # Use custom build instead
      - 'deploy'       # No deploy for this project

tasks:
  build:
    command: 'custom-build-tool'  # Custom implementation

Rename Inherited Tasks

packages/legacy/moon.yml
workspace:
  inheritedTasks:
    rename:
      typecheck: 'type-check'   # Rename typecheck to type-check
      lint: 'check-lint'        # Rename lint to check-lint

Combining Controls

moon.yml
workspace:
  inheritedTasks:
    include:
      - 'test'
      - 'lint'
      - 'build'
    exclude:
      - 'deploy'
    rename:
      test: 'test-unit'

tasks:
  # Override inherited build
  build:
    command: 'custom-builder'
    options:
      mergeArgs: 'replace'

Best Practices

Create separate task files for each toolchain (node.yml, rust.yml, python.yml) as the foundation.
.moon/tasks/
├── node.yml      # All Node.js projects
├── rust.yml      # All Rust projects
└── python.yml    # All Python projects
Add more specific files for stack, layer, or tag combinations.
.moon/tasks/
├── node.yml              # Base Node.js tasks
├── frontend.yml          # Frontend-specific
├── backend.yml           # Backend-specific
└── library.yml           # Library-specific
Define file groups in global tasks to standardize input patterns.
fileGroups:
  sources:
    - 'src/**/*'
  tests:
    - 'tests/**/*'

tasks:
  test:
    inputs:
      - '@globs(sources)'
      - '@globs(tests)'
The default append strategy allows projects to add their specific needs while keeping common configuration.
When using non-default strategies, add comments explaining why.
tasks:
  build:
    inputs:
      - 'custom/**/*'
    options:
      # This project has completely different structure
      mergeInputs: 'replace'

Troubleshooting

Task not inheriting

Check conditions:
# View project details
moon project <project> --json | jq '.language, .stack, .layer, .tags'

# View inherited tasks
moon project <project> --json | jq '.tasks'
Verify inheritedBy conditions match project attributes.

Unexpected merge behavior

Inspect merged task:
# View task details after merging
moon task <project>:<task> --json
Check merge strategy settings in local moon.yml.

Conflicts after inheritance

# Global defines --color
args: ['--color']

# Local adds --no-color (conflict!)
args: ['--no-color']
options:
  mergeArgs: 'append'

# Solution: Use replace strategy
options:
  mergeArgs: 'replace'
  • Tasks - Task configuration and types
  • Projects - Project configuration
  • Workspace - Workspace-level configuration

Configuration Reference

For detailed configuration options, see:

Build docs developers (and LLMs) love