Skip to main content
A workspace is a directory containing multiple XCL stacks. ubx compile --all discovers every stack in the workspace, resolves the dependency order from @name cross-stack references, and compiles each stack in sequence.

Directory layout

Flat workspace

The simplest layout is a flat directory with one subdirectory per stack:
workspace/
  networking/         ← stack 1
    stack.xcl
    input.xcl
    output.xcl
  cluster/            ← stack 2 (depends on @networking)
    stack.xcl
    output.xcl
  addons/             ← stack 3 (depends on @cluster)
    stack.xcl
    output.xcl

Nested workspace

Stacks can be at any depth. Every directory containing .xcl files is treated as an independent stack:
workspace/
  infra/
    networking/       ← stack (exports vpc_id)
      stack.xcl
      input.xcl
      output.xcl
    cluster/          ← stack (depends on @networking)
      stack.xcl
      output.xcl
  apps/
    gitops/           ← stack (exports clone_url)
      stack.xcl
      output.xcl
    addons/           ← stack (depends on @gitops)
      stack.xcl
      output.xcl

Cross-stack references

Use @stackName.outputName to reference an output from another stack:
// cluster/stack.xcl
stack "cluster" {
    eks = aws.eks.Cluster {
        vpc_id     = @networking.vpc_id
        subnet_ids = @networking.subnet_ids
    }
}
The stack name in @name is the directory basename of the dependency.

Sibling discovery rule

When the compiler encounters @networking, it looks for the networking stack at:
<parent of current stack directory>/networking/
For a flat workspace where cluster/ and networking/ are siblings, this resolves correctly:
  • cluster/ → parent is workspace/ → looks for workspace/networking/
For a nested workspace, the same-parent rule applies:
  • infra/cluster/ → parent is infra/ → looks for infra/networking/
  • apps/addons/ → parent is apps/ → looks for apps/gitops/
Cross-domain references are not supported in the current version. An apps/addons stack cannot reference @networking from infra/networking — the compiler would look for apps/networking/ which doesn’t exist.

Compiling a workspace

# Compile all stacks in the current directory (recursive):
ubx compile --all .

# Compile all stacks in a specific directory:
ubx compile --all ./workspace
Output:
● Resolving  dependency graph
             networking/ → cluster/ → addons/
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
● networking/         (1/3)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
● Typecheck  2 file(s)
● IR         building…
● Output     networking/.ubx/__main__.py

✓  compiled 2 file(s) → networking/.ubx/__main__.py
...
✓  all 3 stacks compiled

Dependency resolution

The compiler builds a dependency graph by:
  1. Parsing each stack’s .xcl files (lex + parse — no typechecking yet)
  2. Scanning for @name cross-stack references using the parsed AST
  3. Building an adjacency map: stack → [dependent stacks]
  4. Topological sort (DFS-based) to determine compilation order
Dependencies compile before the stacks that depend on them, so sibling output names are available for validation when a dependent stack is typechecked.

Circular dependency detection

If stacks reference each other in a cycle, the compiler reports a clear error and aborts:
✗  circular dependency: networking → cluster → networking

Duplicate stack names

Two stacks in different subdirectories cannot have the same directory name — this would make @name references ambiguous:
✗  ambiguous stack name "@networking" — found at /workspace/infra/networking and /workspace/staging/networking, rename one directory to resolve
Rename one of the conflicting directories to resolve the ambiguity.

Partial failure

If a stack fails to compile (parse errors, typecheck errors, or output errors), the workspace compile continues to attempt the remaining stacks. A summary is printed at the end:
✗  2/3 stacks failed: cluster, addons
Individual stack errors are printed inline as each stack is processed.

Example: three-stack workspace

workspace/
  networking/
    input.xcl
    stack.xcl
    output.xcl
  cluster/
    stack.xcl
    output.xcl
  addons/
    stack.xcl
    output.xcl
// networking/input.xcl
input "networking" {
    vpc_cidr: string = "10.0.0.0/16"
    azs:      list(string)
}

// networking/stack.xcl
stack "networking" {
    vpc = aws.ec2.Vpc {
        cidr_block = input.vpc_cidr
    }
}

// networking/output.xcl
output "networking" {
    vpc_id = vpc.id
}
// cluster/stack.xcl
stack "cluster" {
    eks = aws.eks.Cluster {
        vpc_id = @networking.vpc_id
    }
}

// cluster/output.xcl
output "cluster" {
    endpoint = eks.endpoint
}
// addons/stack.xcl
stack "addons" {
    argocd = helm.Release {
        cluster_endpoint = @cluster.endpoint
    }
}

// addons/output.xcl
output "addons" {
    status = argocd.status
}
ubx compile --all ./workspace
# Compiles: networking → cluster → addons