Skip to main content
ubx supports three Pulumi runtimes. Every .iac file you write compiles to the same semantics regardless of which runtime you choose; only the generated language changes.

Choosing a runtime

Set the runtime key in ubx.yaml:
name: my-platform
backend: file://.ubx/state
stacks: [dev, staging, prod]

runtime: typescript   # default — can be omitted
# runtime: python
# runtime: go
If runtime is omitted, ubx defaults to typescript.

Generated files per runtime

RuntimeFiles generatedPulumi.yaml runtime
typescript.ubx/index.ts, .ubx/package.jsonnodejs
python.ubx/__main__.py, .ubx/requirements.txtpython
go.ubx/main.go, .ubx/go.modgo
All runtimes also receive .ubx/Pulumi.yaml with the correct runtime: field so Pulumi knows how to execute the stack.

TypeScript (default)

TypeScript is the default and most feature-complete target. It supports all ubx language features including component blocks with local .iac authoring, for_each, count, when, and all five secret() backends.
input "env" {}

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

output "bucket_name" {
  value = unit.aws_s3_bucket_v2.bucket.bucket
}
Generated .ubx/index.ts:
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

const env = new pulumi.Config().require("env");

const bucket = new aws.s3.BucketV2("bucket", {
    bucket: `${env}-assets`,
});

export const bucketName = bucket.bucket;

Python

The Python target generates idiomatic Pulumi Python. Provider packages are imported as pulumi_aws, pulumi_gcp, pulumi_azure_native, etc.
runtime: python
Generated .ubx/__main__.py:
import pulumi
import pulumi_aws as aws

config = pulumi.Config()
env = config.require("env")

bucket = aws.s3.BucketV2("bucket",
    bucket=f"{env}-assets",
)

pulumi.export("bucket_name", bucket.bucket)
Generated .ubx/requirements.txt:
pulumi>=3.0.0,<4.0.0
pulumi-aws>=6.0.0,<7.0.0
Provider versions in requirements.txt default to the latest major. Use a provider block to pin a specific version:
provider "aws" {
  version = "~> 6.0"
}

Go

The Go target generates a single main.go with a pulumi.Run() entry point and a go.mod for the provider dependencies.
runtime: go
Generated .ubx/main.go:
package main

import (
    "github.com/pulumi/pulumi/sdk/v3/go/pulumi"
    "github.com/pulumi/pulumi/sdk/v3/go/pulumi/config"

    "github.com/pulumi/pulumi-aws/sdk/v6/go/aws/s3"
)

func main() {
    pulumi.Run(func(ctx *pulumi.Context) error {
        cfg := config.New(ctx, "")

        env := cfg.Require("env")

        bucket, err := s3.NewBucketV2(ctx, "bucket", &s3.BucketV2Args{
            Bucket: pulumi.String(fmt.Sprintf("%s-assets", env)),
        })
        if err != nil {
            return err
        }

        ctx.Export("bucket_name", bucket.Bucket)

        return nil
    })
}
Generated .ubx/go.mod:
module infra

go 1.21

require (
    github.com/pulumi/pulumi/sdk/v3 v3.0.0
    github.com/pulumi/pulumi-aws/sdk/v6 v6.0.0
)
Provider versions in go.mod default to the latest major. Use a provider block to pin a specific version:
provider "aws" {
  version = "~> 6.2.1"
}
This translates ~> 6.2.1v6.2.1 in go.mod.

Pending<T> compilation per runtime

References to resource outputs (unit.name.attr, component.name.output, @stack.output) resolve at apply time and have type Pending<T>. Each runtime compiles them differently:

TypeScript

Single pending ref:
const copy = src.bucket.apply(srcBucket =>
    new aws.s3.BucketV2("copy", { bucket: srcBucket })
);
Multiple pending refs:
const merged = pulumi.all([a.bucket, b.tagsAll]).apply(([aBucket, bTagsAll]) =>
    new aws.s3.BucketV2("merged", { bucket: aBucket, tagsAll: bTagsAll })
);

Python

Single pending ref:
copy = src.bucket.apply(lambda src_bucket:
    aws.s3.BucketV2("copy", bucket=src_bucket)
)
Multiple pending refs:
merged = pulumi.Output.all(a_bucket=a.bucket, b_tags_all=b.tags_all).apply(
    lambda args: aws.s3.BucketV2("merged",
        bucket=args["a_bucket"],
        tags_all=args["b_tags_all"],
    )
)

Go

Single pending ref:
copy := src.Bucket.ApplyT(func(srcBucket string) (*s3.BucketV2, error) {
    return s3.NewBucketV2(ctx, "copy", &s3.BucketV2Args{
        Bucket: pulumi.String(srcBucket),
    })
}).(s3.BucketV2Output)
Multiple pending refs:
merged := pulumi.All(a.Bucket, b.TagsAll).ApplyT(
    func(args []interface{}) (*s3.BucketV2, error) {
        aBucket := args[0].(string)
        bTagsAll := args[1].(string)
        return s3.NewBucketV2(ctx, "merged", &s3.BucketV2Args{
            Bucket:  pulumi.String(aBucket),
            TagsAll: pulumi.String(bTagsAll),
        })
    },
).(s3.BucketV2Output)

Provider imports per runtime

ProviderTypeScript importPython importGo import
AWSimport * as aws from "@pulumi/aws"import pulumi_aws as awsgithub.com/pulumi/pulumi-aws/sdk/v6/go/aws
GCPimport * as gcp from "@pulumi/gcp"import pulumi_gcp as gcpgithub.com/pulumi/pulumi-gcp/sdk/v7/go/gcp
Azure Nativeimport * as azure from "@pulumi/azure-native"import pulumi_azure_native as azuregithub.com/pulumi/pulumi-azure-native/sdk/v2/go/azure
Kubernetesimport * as k8s from "@pulumi/kubernetes"import pulumi_kubernetes as k8sgithub.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes
Datadogimport * as datadog from "@pulumi/datadog"import pulumi_datadog as datadoggithub.com/pulumi/pulumi-datadog/sdk/v4/go/datadog
Go sub-packages are derived from the resource type. For aws_s3_bucket_v2 the import is ...aws/s3 and the constructor is s3.NewBucketV2.

Sensitive outputs per runtime

The sensitive = true field on an output block wraps the exported value in the runtime’s secret wrapper:
RuntimeGenerated code
TypeScriptpulumi.secret(value)
Pythonpulumi.Output.secret(value)
Gopulumi.ToSecret(value)

Inputs per runtime

ubx inputTypeScriptPythonGo
No defaultnew pulumi.Config().require("name")config.require("name")cfg.Require("name")
With default?? "default"config.get("name") or "default"cfg.Get("name") with fallback
Ephemeralpulumi.secret(config.require("name"))config.require_secret("name")cfg.RequireSecret("name")

Architecture note

All three runtimes share the same compilation pipeline up through the IR (Intermediate Representation) stage:
.iac source
    ↓  parse
    ↓  type-check
    ↓  IR.Build

  ┌─────────────────────────────────┐
  │  IRProgram                      │
  └────┬───────────┬───────────┬────┘
       ↓           ↓           ↓
  TypeScript    Python        Go
  index.ts   __main__.py   main.go
The IR is the clean boundary between language semantics and emitter syntax. Pending<T> resolution, lifecycle rules, depends_on ordering, and all type checking happen once in the IR stage — not per runtime. This guarantees identical behaviour across all three outputs.

Component Runtime Auto-Detection

When no runtime is declared in ubx.yaml or the ubx {} block, ubx inspects local component source directories and auto-detects the stack runtime. The first component with a recognisable entry-point file sets the runtime for the whole stack:
Entry-point fileDetected runtime
__main__.pyPython
index.tsTypeScript
main.goGo
ubx {
  name   = "networking"
  stacks = ["dev", "prod"]
  # runtime omitted — auto-detected from components
}

# ubx finds __main__.py in ../components/vpc → uses Python runtime for the whole stack
component "vpc" {
  source = "../components/vpc"
}
Stacks with no local component blocks, or with only git:: sources, fall back to TypeScript when no runtime is set explicitly. The stack-level runtime key is still supported and takes precedence over auto-detection — set it explicitly when you want to lock the runtime regardless of component contents:
# ubx.yaml — explicit runtime always wins
runtime: python