Skip to main content
There’s a gap at the heart of every infrastructure-as-code toolchain, and most teams patch it with CI scripts, SSM parameters, or manual copy-paste. The gap is this: your infrastructure tool provisions a database and knows its endpoint, but your application deployment tool needs that endpoint to configure the app — and the two tools have no native way to pass the value between them. So teams write glue. They add a CI step that extracts the Terraform output, stashes it in a secret store, and injects it into a Helm values file. It works until it breaks, and it breaks every time the infrastructure changes in a way the glue didn’t anticipate. ubx eliminates this gap at the language level with the Pending<T> type system. Writing unit.aws_rds_instance.db.endpoint directly as a block attribute value marks it as Pending<T> — a value that doesn’t exist yet at compile time but will exist at apply time. ubx tracks these pending values through the compilation pipeline and generates the correct Pulumi Output<T>.apply() chains so that Pulumi automatically waits for the database to exist before deploying the Helm chart. No scripts. No secret stores. No glue.

What you’ll learn

  • The Pending<T> type system — ubx’s core innovation
  • How infrastructure outputs flow automatically into application configuration
  • How ubx compiles Pending<T> references to Pulumi Output<T>.apply() chains

Why this matters

The Pending<T> system isn’t just about convenience — it’s a correctness guarantee. By expressing the dependency in the language, ubx ensures Pulumi can never deploy the Helm chart before the database is ready. There’s no race condition, no “apply twice”, no manual ordering.

The source code

# Provision a PostgreSQL RDS instance
unit "aws_rds_instance" "db" {
  engine              = "postgres"
  engine_version      = "15"
  instance_class      = "db.t3.micro"
  db_name             = "appdb"
  username            = "admin"
  password            = secret("aws_secrets_manager", "/myapp/db/password")
  skip_final_snapshot = true
}

# Deploy a Helm chart — wiring the RDS endpoint in automatically.
# unit.aws_rds_instance.db.endpoint is Pending<T>: it resolves at apply time,
# after the RDS instance exists and has an endpoint.
deploy "helm" "backend" {
  chart     = "my-backend"
  namespace = "default"
  values = {
    database = {
      host = unit.aws_rds_instance.db.endpoint  # Pending<string>
      port = 5432                                 # Resolved<int>
      name = "appdb"                              # Resolved<string>
    }
  }
}

How it works

1

Resource output references are Pending<T>

unit.aws_rds_instance.db.endpoint is a resource output reference — ubx classifies it as Pending<string> because it only exists after Pulumi has created the RDS instance. The compiler tracks this classification through the entire deploy block.
2

Pending propagates through the block

Because database.host is Pending<T>, the entire values object becomes Pending<T>. ubx records the deploy block’s pending refs — the specific values that drive the generated .apply() wrapping.
3

The IR pass classifies every property

During AST → IRProgram, each property is marked Pending: true or Pending: false. The emitter uses this classification to decide which resources need .apply() chains and which are plain constructors.
4

The emitter generates the .apply() chain

The TypeScript emitter wraps the deploy block in db.endpoint.apply(endpoint => ...). Pulumi waits for db.endpoint to resolve before creating the Helm release — automatically, with no manual depends_on.

What ubx generates

// Generated by ubx — do not edit
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
import * as k8s from "@pulumi/kubernetes";

const db = new aws.rds.Instance("db", {
    engine: "postgres",
    engineVersion: "15",
    instanceClass: "db.t3.micro",
    dbName: "appdb",
    username: "admin",
    password: pulumi.secret("..."),
    skipFinalSnapshot: true,
});

// Pulumi waits for db.endpoint before creating this Chart
const backend = db.endpoint.apply(endpoint =>
    new k8s.helm.v3.Chart("backend", {
        chart: "my-backend",
        namespace: "default",
        values: {
            database: {
                host: endpoint,
                port: 5432,
                name: "appdb",
            },
        },
    })
);

export const dbEndpoint = db.endpoint;

Common mistakes

input.* references are Resolved<T> — they are available at compile time and do not require a pending chain. Only unit.*, component.*, and data.* attribute references are Pending<T>.
Pending<T> propagates automatically. If any property in a block is Pending<T>, ubx generates the minimum required .apply() wrapping — you don’t need to mark every property. ubx figures out exactly which refs need to be in the .apply() closure and generates the tightest possible chain.

Run it

ubx validate   # type-checks the Pending<T> references locally
ubx plan       # requires AWS + Kubernetes credentials
ubx apply      # provisions RDS first, then deploys Helm chart with the real endpoint

What you learned

Resource output references (unit.*, component.*, data.*) are Pending<T> — they resolve at apply time, not compile time
Pending<T> propagates through block values automatically
ubx generates correct Pulumi Output<T>.apply() chains — no manual depends_on needed
Infrastructure outputs wire directly into application config with zero manual steps

Next steps

Conditional expressions

Use ??, if/else, switch, and more

Pending type concept

Deep dive into the Pending<T> type system