Skip to main content
Code hierarchy lets you place organisation-wide defaults — backend, tags, stacks, and provider versions — in a root config that every child project automatically inherits. Child projects can override any field; on conflict, the child always wins. This mirrors the Terragrunt pattern: a monorepo root sets the baseline, individual service directories customise what they need.

File structure

./                               ← root config lives here
  ubx.root.yaml                  ← org-wide defaults (backend, tags, stacks)
  ubx.root.iac                   ← org-wide provider versions (optional)

  services/
    api/
      ubx.yaml                   ← child project — overrides what it needs
      main.iac
      inputs.iac
    worker/
      ubx.yaml
      main.iac
Scaffold this structure with one command:
ubx init --hierarchy

Root config: ubx.root.yaml

# ubx.root.yaml — place at the monorepo root

backend: s3://my-org-bucket/state

default_stack: dev
stacks:
  - dev
  - staging
  - prod

default_tags:
  managed: ubx
  team: platform

Supported fields

FieldTypeDescription
backendstringPulumi state backend URL (e.g. s3://bucket/state, file://.ubx/state)
default_stackstringDefault stack name when --stack is not set
stackslist of stringsValid stack names for the organisation
default_tagsmapResource tags applied to all taggable units (deep-merged with child tags)

Root providers: ubx.root.iac

Declare provider version constraints that apply to all child projects:
# ubx.root.iac — shared provider versions

provider "aws" {
  version = "~> 6.0"
}

provider "gcp" {
  version = "~> 7.0"
}
ubx.root.iac supports provider blocks only. The provider constraints are merged into the child’s package.json at compile time — you don’t need a provider block in every child project.

Child project: ubx.yaml

A child project only needs to declare what differs from the root:
# services/api/ubx.yaml — child project

project: api

# backend, stacks, and default_tags are inherited from ubx.root.yaml.
# Uncomment to override:
# backend: s3://my-team-bucket/state
# stacks: [dev, staging]
# default_stack: staging

Tag merge example

Root ubx.root.yaml:
default_tags:
  managed: ubx
  team: platform
Child ubx.yaml:
default_tags:
  env: prod
  team: api-team       # overrides root's "team" value
Effective tags applied to all resources in the child project:
managed: ubx           ← from root
team: api-team         ← child overrides root
env: prod              ← from child only
Child values always win on conflict. Tags that only appear in the root are always inherited.

Child providers: extend and override

A child project can declare its own provider block to use a different version than the root:
# services/api/providers.iac

provider "aws" {
  version = "~> 6.1"   # overrides the root's "~> 6.0" for this project only
}
Same-name provider → child version is used. Providers declared only in the root are inherited automatically with no declaration needed in the child.

Merge rules

FieldRule
backendChild wins if declared; otherwise inherit root
default_stackChild wins if declared; otherwise inherit root
stacksChild wins if declared (even partial list); otherwise inherit root
default_tagsDeep merge — root provides base; child overwrites on key conflict
Provider blocksUnion — root provides baseline; same-name child block wins entirely
ubx.root.iac absentTreated as no root providers — not an error

Root discovery

When you run any ubx command from a child directory, ubx automatically walks up the directory tree looking for ubx.root.yaml:
services/api/    ← start here
services/        ← checked, no ubx.root.yaml
./               ← found! ubx.root.yaml here
Walking stops at the first ubx.root.yaml found, or at a .git directory (single-project mode — no root applied, no error).
monorepo/
  .git/          ← boundary: stops search here
  services/
    api/         ← starts here, stops at .git above — no root applied
This prevents accidentally inheriting config from outside your git repository.

--root flag

Override discovery with an explicit root directory:
ubx validate --root /path/to/root
ubx plan     --root ../shared-root
ubx apply    --root ../shared-root
The --root flag is available on validate, plan, apply, and destroy.

Root line in output

When a root config is active, ubx shows it before the compile phase:
  ◆ Root       ../.. (ubx.root.yaml)
  ◆ Compile    stack.iac

  ✓  valid
If no root is found, the Root line is absent — single-project mode runs as before.

Scaffold with ubx init --hierarchy

The --hierarchy flag creates the full root + child structure in one step:
ubx init --hierarchy           # scaffold in current directory
ubx init myproject --hierarchy # scaffold into myproject/
Generated structure:
./
  ubx.root.yaml                  ← org defaults (backend, stacks, tags, managed=ubx)
  ubx.root.iac                   ← shared aws provider ~> 6.0
  services/
    api/
      ubx.yaml                   ← minimal child (inherits everything from root)
      main.iac                   ← sample S3 bucket + output
      inputs.iac                 ← region input

Running Commands Across All Projects

The --all flag runs validate, plan, or apply across every child project discovered under the root, in a single command.

Basic usage

# From the root or any child directory — discovers root automatically
ubx validate --all
ubx plan     --all
ubx apply    --all --yes
You can run from the root directory or from any child directory. ubx walks up to find ubx.root.yaml and then discovers all child projects beneath it.

Output format

  ◆ Workspace  3 project(s) found

  ◆ Validate   services/api
  ✓  services/api

  ◆ Validate   services/worker
  ✓  services/worker

  ◆ Validate   infra/networking
     ◆ Root       ../.. (ubx.root.yaml)
     ◆ Compile    stack.iac
     ✗  stack.iac:6  unknown attribute "fake_attr"
     ✗  1 error(s)
  ✗  infra/networking

  ────────────────────────────────────────
  2 passed · 1 failed

--filter — run a subset of projects

# Exact path match
ubx validate --all --filter services/api

# Glob pattern (quote to prevent shell expansion)
ubx validate --all --filter 'services/*'
ubx validate --all --filter 'infra/*'
Paths are relative to the root directory. Exact match is tried first; then filepath.Match glob semantics are applied.

--parallel — concurrent execution

ubx validate --all --parallel 4    # validate 4 projects concurrently
ubx plan     --all --parallel 2
Default is 1 (sequential). Output is printed in project order regardless of completion order.

--continue-on-error — collect all results

ubx validate --all --continue-on-error
Without --continue-on-error, --all stops on the first failure and marks remaining projects as skipped:
  ◆ Validate   infra/networking
  ✗  infra/networking

  ◆ Validate   services/api
  ·  skipped

  ◆ Validate   services/worker
  ·  skipped

  ────────────────────────────────────────
  0 passed · 1 failed · 2 skipped
With --continue-on-error, every project runs regardless of failures. The summary shows final counts:
  2 passed · 1 failed

Error: no root found

If ubx.root.yaml is not found in any parent directory, --all exits immediately:
Error: --all requires a ubx.root.yaml in a parent directory
Use the --root flag to point to an explicit root when outside the discovery path:
ubx validate --all --root /path/to/root

--all flag reference

FlagDefaultDescription
--allfalseRun across all child projects under the root
--filter string(all)Exact path or glob pattern to restrict which projects run
--parallel int1Number of projects to run concurrently
--continue-on-errorfalseKeep running even after a failure (default stops on first)

Complete example

ubx.root.yaml (monorepo root):
backend: s3://my-org-state/state
default_stack: dev
stacks: [dev, staging, prod]
default_tags:
  managed: ubx
  team: platform
ubx.root.iac (monorepo root):
provider "aws" {
  version = "~> 6.0"
}
services/api/ubx.yaml (child project):
project: api
default_tags:
  service: api
  env: "${input.env}"
services/api/main.iac (child project):
input "env" {
  type    = "string"
  default = "dev"
}

unit "aws_s3_bucket_v2" "uploads" {
  bucket = "api-uploads-${input.env}"
}

output "uploads_bucket" {
  value = unit.aws_s3_bucket_v2.uploads.bucket
}
Running from services/api/:
ubx validate
# ◆ Root       ../.. (ubx.root.yaml)
# ◆ Compile    stack.iac
# ✓  valid

ubx plan
# ◆ Root       ../.. (ubx.root.yaml)
# ◆ Compile    stack.iac → .ubx/index.ts
# ...
Effective config for services/api/:
  • Backend: s3://my-org-state/state (from root)
  • Stacks: [dev, staging, prod] (from root)
  • Tags: {managed: ubx, team: platform, service: api, env: "${input.env}"} (merged)
  • AWS provider: ~> 6.0 (from root — no override in child)