During `pulumi preview` of a stack whose resources do not exist yet, what does an Output.apply callback see, and what does that imply about the code you put inside apply?
answer
- preview creates nothing
- unknown propagates, callback skipped
- console.log inside apply never prints
- resources born inside apply skip the review
- isDryRun is a plain boolean
basics
~20 sOn a preview, values from not-yet-created resources are unknown, so Pulumi skips those apply callbacks and propagates an unknown instead of running them. Code inside apply therefore may never execute at preview time — keep it a pure transformation with no side effects and no resource creation.
solid answer
~50 sDuring `pulumi up` the engine creates resources and then resolves their Outputs, so applies run on real values. During `pulumi preview` nothing is created, so for a resource that does not exist yet its attributes are marked *unknown*; Pulumi does not invoke the apply callback at all and the derived Output stays unknown, which is why previews render such properties as `output<string>` rather than a diffable value. Practical consequences: logging or an HTTP call inside apply silently does not happen at preview; a resource created inside an apply does not appear in the preview and then materialises during the update, which is exactly the surprise you don't want in a change review; and any diff computed from an unknown is uninformative. Keep applies pure and side-effect free, declare resources at program top level, and if you genuinely need to branch on being in a preview, use `pulumi.runtime.isDryRun()` — a plain boolean, not an Output.
code
typescript · 13 linesimport * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const bucket = new aws.s3.Bucket("logs");
// On a preview of a bucket that does not exist yet, this callback is skipped:
// the arn is unknown, so Pulumi propagates an unknown instead of calling it.
export const upper = bucket.arn.apply(arn => arn.toUpperCase());
// A plain boolean, safe to branch on:
if (pulumi.runtime.isDryRun()) {
pulumi.log.info("running a preview, no changes will be made");
}go deeper
Know that a preview creates nothing, so values from new resources are unknown and show up as output<string> rather than a real ARN or name.
Explain that Pulumi skips the apply callback on an unknown and propagates the unknown instead, and that unknown-ness spreads to everything derived from it.
Show the operational consequence: applies must be pure, resources belong at top level so the preview is a faithful change record, and a preview full of unknowns for existing resources is a signal worth investigating.
Own the review-integrity argument — a preview that under-reports what an update will create undermines the approval gate itself, so ban resource creation inside applies at the codebase level rather than case by case.
## Two runs, two states of knowledge A Pulumi deployment runs your program twice in spirit: once to *show* you what would change (`pulumi preview`, also the implicit preview that `pulumi up` shows before applying) and once to actually make the changes. The program itself is identical. What differs is how much the engine knows. On an update, the engine creates or updates each resource and gets the provider's real response back — the ARN, the generated name, the endpoint. Those values are known, so any `Output.apply` chained off them runs with a real argument. On a preview, nothing is created. For a resource that already exists and is unchanged, the engine can supply the previously recorded values, so its Outputs are known and applies over them run normally. For a resource that does not exist yet — a brand-new stack, or a new resource added to an existing one — the provider cannot tell you what the ARN will be. Pulumi marks that Output **unknown**. ## What Pulumi does with an unknown It does not call your callback with `undefined` and hope for the best. It skips the callback and produces an unknown Output as the result. Unknown-ness then propagates: anything derived from an unknown is unknown, and the preview displays such properties as `output<string>` instead of a concrete value. That is the same phenomenon a Terraform user recognises as `(known after apply)` — a diff that cannot be fully computed because half the inputs do not exist yet. This is a correctness decision. Running your callback on a placeholder would produce a plausible-looking but wrong string, and that string would then be shown in the preview as if it were the planned value. ## The three things this breaks **Side effects.** A `console.log` inside apply is the standard "why isn't my debugging printing" bug: ```typescript bucket.arn.apply(arn => { console.log("arn:", arn); return arn; }); ``` On a first preview this never prints. Neither does a metrics call, a file write or an HTTP POST. Anything you rely on happening should not be sitting inside an apply. **Resource creation inside apply.** Technically possible, and a well-known anti-pattern: ```typescript // Anti-pattern: the child does not show up in the preview cluster.endpoint.apply(ep => new SomeResource("child", { endpoint: ep })); ``` Because the callback is skipped at preview, the resource is not registered and does not appear in the change list. Then `up` runs, the callback fires, and the resource is created — infrastructure appearing that the review never showed. Worse, resources registered late this way can be missed by the engine's snapshot bookkeeping. The right shape is to declare the resource at top level and pass the Output straight into its arguments as an `Input<T>`, letting the dependency edge do the sequencing. **Diff quality.** If a policy document, a user-data script or a tag map is built by applying over a not-yet-known value, the preview simply cannot show what it will contain. That is unavoidable for genuinely new resources, but it is worth noticing when it happens to *existing* resources: it usually means you introduced an artificial dependency on something new. ## When you really do need to know `pulumi.runtime.isDryRun()` returns a plain boolean — true during a preview, false during an update. Python's equivalent is `pulumi.runtime.is_dry_run()`. Because it is a plain value and not an Output, it is safe in an `if`. Use it sparingly. Branching your resource graph on it means the preview describes a different program from the one that is applied, which defeats the point of previewing. Legitimate uses are narrow: skipping an expensive external lookup used only for a computed value, or providing a placeholder for something purely cosmetic. ## Related traps in the same family - An apply callback can run more than once across a session, and its result is cached per Output, so treat it as a pure function of its input. - Randomness or `Date.now()` inside an apply produces a value that changes between preview and update, showing spurious diffs. Non-determinism belongs in a resource that records its value — the `random` provider exists for exactly this. - Reading an environment variable or a local file at program top level is fine and is known at preview; the same read inside an apply may never execute. The rule that covers all of it: **applies transform values, they do not perform work.**
- Why is creating a resource inside an apply callback considered an anti-pattern rather than just unusual?Because the callback is skipped when the input is unknown, so the resource is invisible in the preview and then appears during the update — infrastructure that no reviewer approved. Declaring it at top level and passing the Output into its arguments gives the same ordering through the dependency edge, with a preview that tells the truth.
- Does an apply over an existing, unchanged resource's output also get skipped at preview?No. The engine has that resource's recorded values, so its Outputs are known and the callback runs normally at preview. Skipping is specific to unknowns — resources being created, or values downstream of one.
- How does this compare to what a Terraform user sees?It is the same phenomenon under a different label: Terraform prints `(known after apply)` for attributes the provider cannot compute until create time, and anything derived from one is equally unknown. Both tools choose to show an incomplete diff rather than a confidently wrong one.
saying these in an interview costs you the question
- Thinks apply runs at preview with a placeholder string
- Debugs a program with console.log inside apply
- Creates resources inside apply to sequence them
- Assumes preview output<string> means something is broken
- Branches the whole resource graph on isDryRun