Open-Source Wikis

/

Pulumi

/

Primitives

/

Inputs and Outputs

pulumi/pulumi

Inputs and Outputs

Active contributors: Justin Van Patten, Ian Wahbe, Thomas Gummerer

Purpose

Pulumi programs run eagerlynew Bucket(...) returns immediately, before the bucket exists in the cloud. Yet the bucket's id, ARN, and other outputs aren't known until after the engine has called Create. The Output<T> type bridges these two timelines: it represents a value that will exist, plus enough metadata for the engine to build a dependency graph.

Input<T> is the dual: it's "anything that can produce a T" — a literal, a Promise, or another Output. Resources accept Input<T> for their fields; they hand out Output<T> on their outputs.

What an Output carries

Conceptually, every Output<T>:

  1. A value (or a future for it).
  2. A set of resource dependencies (URNs whose outputs flowed into this one).
  3. A "is known?" bit (during pulumi preview, many Outputs are still unknown).
  4. A "is secret?" bit (sticky — once secret, always secret).

This gives the engine enough to schedule resource creates in the right order and to redact logs.

Per-language implementations

Language File Pattern
Go sdk/go/pulumi/types.go + types_builtins.go (~480 KB generated) Typed <T>Output interfaces (StringOutput, Int64MapOutput, ...)
Go (modern) sdk/go/pulumix/ Generic Output[T]
Node sdk/nodejs/output.ts (~53 KB) Class Output<T> with .apply<U>(fn: (t: T) => U | Output<U>): Output<U>
Python sdk/python/lib/pulumi/output.py (~40 KB) Output[T] built on asyncio.Future

All three languages converge on the same algebra:

  • Lift: a literal value lifts via pulumi.output(value) (or pulumi.Output(value) in Python).
  • Apply: output.apply(fn) runs fn once the inner value is known and returns a new Output.
  • All: pulumi.all([a, b, c]).apply(([va, vb, vc]) => ...) joins many.
  • Secret: pulumi.secret(value) marks an Output secret; the bit is sticky.
  • Unsecret: Provider authors can pulumi.unsecret(...) (rare; usually a bug).

How dependencies are tracked

When a resource is registered, the SDK collects every Output that appears in its inputs. The set of URNs from those Outputs becomes the Dependencies (and per-property PropertyDependencies) of the new resource. This is automatic — users don't think about it. The engine uses these to:

  • Order step execution.
  • Decide replacement scope when a property change forces a replace.
  • Compute the "what would be affected" set in --target.

Unknown values during preview

In a pulumi preview, the engine doesn't actually call Create on the cloud. So an Output of "the bucket's ARN" can't be known. The SDK marks such Outputs unknown. The user's apply callbacks may not run, or run with unknown sentinel values. SDKs surface this:

  • Go: <T>Output.ApplyT is invoked only if the value is known; otherwise the result is also unknown.
  • Node: Output<T>.apply(fn) — if the input is unknown, the result is unknown without calling fn.
  • Python: same idea.

Bugs related to "the apply ran during preview but the value was undefined" are usually about user code reaching into an Output incorrectly during a preview — typically resolved by ensuring the apply path is safe for unknown.

Secrets are sticky

Once an Output is secret, the secret bit propagates through every transformation:

const password = pulumi.secret(config.require('password'));
const url = password.apply((p) => `db://...:${p}@host`);
// url is also secret

When the engine writes the snapshot, secret-marked properties are encrypted by the configured secrets manager. See systems/secrets.

Per-property dependencies

PropertyDependencies map[PropertyKey][]URN lets the engine know which input fields depend on which other resources. This is finer-grained than the resource-level Dependencies and matters for correct replacement: a change to one property may force replace, while another property might be independent.

The SDKs track this automatically by recording, for each property, which Outputs were consumed. The engine uses it during diff to decide blast radius.

Asset and Archive — first-class file types

Two special property types live alongside primitives:

  • Asset — a single file or string blob. Variants: FileAsset(path), StringAsset(s), RemoteAsset(url).
  • Archive — a directory or tarball. Variants: FileArchive(path), RemoteArchive(url), AssetArchive({path: asset, ...}).

They're carried in the wire as Asset/Archive PropertyValues and have their own protobuf message types (proto/pulumi/asset.proto, sdk/go/common/resource/asset.go).

Hashing:

  • Assets carry a SHA256 hash. The engine compares hashes for diff.
  • Archives carry a hash of the contents (sorted file tree). Adding a file rehashes the archive.

Remote/computed in the wire

The protobuf has dedicated tags for "this property is unknown" and "this property is a secret". sdk/go/common/resource/properties.go provides MakeComputed(...) and MakeSecret(...) helpers; SDKs use them when marshaling.

Stack references

StackReference reads outputs from another stack. The result is an Output preserving the secret bit.

const network = new pulumi.StackReference('network/prod');
const subnet = network.getOutput('subnetId');

Implementation: pkg/resource/deploy/builtins.go (engine-side) plus per-language wrappers (sdk/go/pulumi/stack_reference.go, sdk/nodejs/stackReference.ts, sdk/python/lib/pulumi/stack_reference.py).

Entry points for modification

  • Output behavior change — touch every SDK. Cross-language conformance tests will catch divergence.
  • A new property typePropertyValue in sdk/go/common/resource/properties.go, plus serialization in sdk/go/pulumi/rpc.go and equivalents in Node/Python.
  • A new built-in (like secret, unsecret) — typically requires a built-in invoke (pkg/resource/deploy/builtins.go) plus an SDK helper.

See also

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Inputs and Outputs – Pulumi wiki | Factory