Skip to main content
The workspace block sets workspace-wide defaults for engine, runtime, backend, providers, tags, hooks, plugins, and feature toggles. It applies to every stack in the compilation unit unless a more-specific declaration overrides a field. Multiple workspace blocks may appear across .xcl files in the same directory. The blocks are independent — each can carry a match {} sub-block to restrict which stacks it applies to. When no match {} is present, the block is universal: it applies to all stacks.

Syntax

workspace {
    name    = "platform"
    engine  = "native"
    runtime = "xcl"
}
workspace takes no label — there is no quoted string after the keyword. The human-readable name for the workspace comes from the name field inside the body. Only one backend {} sub-block is allowed per workspace {}. Duplicate backends produce a hard parse error.

Fields

FieldTypeDefaultDescription
namestring""Human-readable workspace name
enginestring"native"Execution engine (see below)
runtimestring"xcl"Codegen runtime (see below)

engine

Controls which execution engine processes the stack.
ValueDescription
"native"Built-in ubx engine (default)
"pulumi"Pulumi execution plugin
"terraform"Terraform execution plugin — not yet implemented, produces a hard error at ship time

runtime

Controls which code generation target is used.
ValueDescription
"xcl"XCL native — no codegen step (default)
"python"Pulumi Python
"typescript"Pulumi TypeScript
"go"Pulumi Go

Valid engine / runtime combinations

Not all engine/runtime pairs are valid. The typechecker enforces these rules:
engineValid runtimes
nativexcl only
pulumipython, typescript, go
terraformxcl only
// Valid — native engine with xcl runtime
workspace {
    engine  = "native"
    runtime = "xcl"
}

// Valid — pulumi engine with python runtime
workspace {
    engine  = "pulumi"
    runtime = "python"
}

// Error — xcl runtime requires native engine
workspace {
    engine  = "pulumi"
    runtime = "xcl"    // ✗ runtime "xcl" requires engine "native"
}

match sub-block

The match {} sub-block turns a universal workspace into a label-scoped workspace. A label-scoped workspace only applies to stacks whose resolved labels (from their meta {} block and the matched context {}) contain all the key-value pairs declared in match {}.
workspace {
    name = "platform-prod"
    match {
        env = "prod"
    }
    backend s3 {
        bucket = "prod-state"
        key    = "platform/prod"
    }
}
If a stack’s effective labels include env = "prod", this workspace block applies on top of any universal workspace. Fields from the scoped workspace override the universal workspace for that stack. A workspace block with no match {} is universal — it applies to every stack and is processed first in the resolution chain.

backend sub-block

Declares the Pulumi state backend. See backend for the full reference.
workspace {
    backend s3 {
        bucket = "my-state-bucket"
        key    = "platform/prod"
        encryption {
            provider = "awskms"
            key      = "alias/pulumi-secrets"
        }
    }
}

provider sub-block

Declares provider defaults applied to all stacks that inherit this workspace.
workspace {
    provider aws {
        region  = "us-east-1"
        version = ">= 6.0.0"
    }
}

tags sub-block

Key-value pairs applied as resource tags for all stacks that inherit this workspace. Tags merge additively across the resolution chain — a tag set at the workspace level is not removed by a more specific block; it is extended.
workspace {
    tags {
        project     = "platform"
        managed_by  = "ubx"
    }
}

hooks sub-block

Shell commands to run before or after a ubx ship or ubx plan operation.
workspace {
    hooks {
        before_ship = ["echo 'Starting deploy'", "./scripts/pre-deploy.sh"]
        after_ship  = ["./scripts/notify-slack.sh"]
        before_plan = ["./scripts/validate-config.sh"]
        after_plan  = []
    }
}
FieldTimingOn failure
before_shipBefore the deploy startsAbort the deploy (fail-fast)
after_shipAfter a successful deployLog error, do not fail the operation
before_planBefore the plan startsAbort the plan (fail-fast)
after_planAfter a successful planLog error, do not fail the operation
Each element is a shell command string. Commands run via sh -c "<command>" in the stack directory. On a non-zero exit code, before_* hooks immediately abort the operation; after_* hooks log the error and continue. Hooks merge additively across the resolution chain — hooks from workspace, context, and meta are concatenated into a single list in resolution order.

plugins sub-block

Declares plugins to load for stacks that inherit this workspace.
workspace {
    plugins {
        my_scanner { source = "strata://scanners/my-scanner" }
        cost_est   { source = "github://ubiquex/cost-estimator" }
        local_lint { source = "file://./tools/linter" }
    }
}
Each plugin entry has:
  • A name (bare identifier — use underscores, not hyphens: my_scanner not my-scanner)
  • A source field specifying where to fetch the plugin

Plugin source schemes

SchemeExampleDescription
strata://strata://scanners/my-scannerStrata plugin registry
github://github://ubiquex/cost-estimatorGitHub repository
file://file://./tools/linterLocal filesystem path
Sources with an unrecognized scheme produce a warning (not an error). The stack still compiles.

ai sub-block

workspace {
    ai {
        enabled = true
        model   = "claude-haiku-4-5"

        features {
            plan_summary  = true
            cost_estimate = true
        }
    }
}

scan sub-block

workspace {
    scan {
        enabled  = true
        severity = "high"
    }
}

telemetry sub-block

workspace {
    telemetry {
        enabled = true
    }
}

history sub-block

workspace {
    history {
        enabled = true
    }
}

Multiple workspace blocks

Multiple workspace blocks are allowed in the same compilation unit. They are merged per-field during resolution — a label-scoped workspace does not replace the universal workspace; it overrides only the fields it declares.
// Universal — all stacks get this backend and these tags
workspace {
    name   = "platform"
    engine = "native"
    backend s3 {
        bucket = "state-default"
        key    = "platform"
    }
    tags {
        project = "platform"
    }
}

// Prod-scoped — stacks with env=prod get a different backend
workspace {
    name = "platform-prod"
    match {
        env = "prod"
    }
    backend s3 {
        bucket = "state-prod"
        key    = "platform/prod"
    }
}
A stack whose labels include env = "prod" sees the prod-scoped backend. A stack without that label sees the default backend. Both stacks see tags.project = "platform" from the universal block.

Where workspace blocks live

workspace {} blocks can appear in any .xcl file. There is no required filename — discoverable in workspace.xcl, config.xcl, or any other file in the stack directory.

Resolution chain

The full resolution order is:
  1. Universal workspace {} (no match {}) — lowest priority
  2. Label-matched workspace {} — fields override step 1
  3. context {} named by the stack’s meta { context = "..." } — fields override step 2
  4. meta {} inside stack {} — highest priority
Tags and hooks from all levels are merged additively. All other fields: most specific wins. See meta for the full resolution reference.

Error conditions

Invalid engine value

engine "lambda" is not valid — valid values: native, pulumi, terraform

Invalid runtime value

runtime "ruby" is not valid — valid values: xcl, python, typescript, go

Incompatible engine / runtime

runtime "xcl" requires engine "native" — engine "pulumi" is not compatible
runtime "python" requires engine "pulumi" — engine "native" is not compatible

Unrecognized plugin source scheme (warning)

warning: plugin "my-plugin" source "npm://my-plugin" has an unrecognized scheme — expected strata://, github://, or file://

Duplicate backend block

duplicate backend block — only one backend {} is allowed inside workspace {}