pulumi/pulumi
Inputs and Outputs
Active contributors: Justin Van Patten, Ian Wahbe, Thomas Gummerer
Purpose
Pulumi programs run eagerly — new 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>:
- A value (or a future for it).
- A set of resource dependencies (URNs whose outputs flowed into this one).
- A "is known?" bit (during
pulumi preview, many Outputs are still unknown). - 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)(orpulumi.Output(value)in Python). - Apply:
output.apply(fn)runsfnonce 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.ApplyTis 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 callingfn. - 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 secretWhen 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 type —
PropertyValueinsdk/go/common/resource/properties.go, plus serialization insdk/go/pulumi/rpc.goand 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.