Inputs, Outputs and the Dependency Graph

Authors

K19G

grok-bot

Updated

October 31, 2026

Inputs, Outputs and the Dependency Graph

A Pulumi program looks imperative: Python statements, run top to bottom. The infrastructure it describes is declarative: Pulumi decides what to create, in what order, and what to leave alone. The bridge between the two is a pair of types. Inputs are what you give a resource. Outputs are values the cloud reports back later. Every confusing moment for new Pulumi users (“why does my bucket name print as an object?”) comes from treating an output as if it were already a value.

What was and was not run

pulumi and the provider SDKs were not available on the authoring box and installs were not allowed, so these Python snippets were not executed. They follow the current Pulumi documentation and SDK method names.

Learning goals

  • Distinguish inputs, resource outputs and stack outputs
  • Explain how passing an output into an input creates a dependency edge
  • Transform outputs with apply, Output.all, Output.format, Output.concat and the JSON helpers, and reach nested values by lifting
  • Recognise the “output used as a plain value” bug, and why resources must not be created inside apply

Inputs

An input is an argument to a resource, usually one parameter of the underlying cloud API call. Every input of type Input[T] also accepts a plain T:

import pulumi_random as random

my_id = random.RandomId("mine", byte_length=8)   # 8 is a plain value used as an input

Python offers two spellings for object-shaped inputs: an ...Args class or a dictionary typed as ...ArgsDict. Both carry the same fields, and the dictionary form is shorter:

import pulumi_aws as aws

repo = aws.ecr.Repository(
    "repo",
    image_tag_mutability="MUTABLE",
    image_scanning_configuration={"scan_on_push": True},
)

Outputs

An output is a property the provider reports after the resource exists, such as an ID, an ARN, an endpoint or a generated password. Outputs behave like futures. During a first pulumi up the value is unknown until the cloud answers. During pulumi preview it may never be known.

Pass an output straight into another resource’s input and Pulumi records a dependency:

import pulumi_aws as aws
import pulumi_random as random

password = random.RandomPassword("password", length=16, special=True)

db = aws.rds.Instance(
    "db",
    instance_class="db.t3.micro",
    allocated_storage=64,
    engine="mysql",
    username="someone",
    password=password.result,      # output -> input: an edge in the graph
)

That edge tells Pulumi to create the password before the database, and on destroy to delete the database before the password. You wrote no ordering code. A program that wires outputs to inputs gets correct ordering for free, and depends_on is only for dependencies that no value carries.

Three similar names refer to different things:

Term What it is
Input An argument to a resource; accepts plain values or outputs
Resource output A property of a resource, known once the provider reports it
Stack output A value you pulumi.export at the end of a run for the CLI or a StackReference

Getting at the value

Because an output is not yet a value, you can’t use ordinary language operations on it. print(bucket.id) or "https://" + lb.dns_name doesn’t do what you expect. The SDK supplies the tools instead.

apply: transform one output

url = lb.dns_name.apply(lambda name: f"https://{name}")

The callback runs when the value is known and receives the plain value. Its return value becomes a new Output, which you can pass on as an input or export. Secret-ness and dependencies flow through.

Output.all: combine several

import pulumi

conn = pulumi.Output.all(server.name, database.name).apply(
    lambda args: f"Server=tcp:{args[0]}.database.windows.net;initial catalog={args[1]};"
)

# keyword form yields a dict
conn = pulumi.Output.all(server=server.name, db=database.name).apply(
    lambda a: f"Server=tcp:{a['server']}.database.windows.net;initial catalog={a['db']};"
)

Helpers: skip the lambda

Most apply calls build a string or some JSON. The helpers do that directly and are easier to read:

import json
import pulumi

url = pulumi.Output.format("https://{0}/api", lb.dns_name)
arn_path = pulumi.Output.concat(bucket.arn, "/*")

policy = pulumi.Output.json_dumps({
    "Version": "2012-10-17",
    "Statement": [{
        "Effect": "Allow",
        "Principal": "*",
        "Action": ["s3:GetObject"],
        "Resource": [arn_path],          # outputs allowed inside the structure
    }],
})

parsed = pulumi.Output.json_loads(some_json_output)

json_dumps accepts a structure that has outputs nested inside it. A json.dumps inside an apply would make you resolve each one by hand first.

Lifting: reach into nested outputs

Properties and indexes can be read straight off an output, with no apply. Pulumi “lifts” the access:

record_name = cert.domain_validation_options[0].resource_record_name

Without lifting this would be cert.domain_validation_options.apply(lambda o: o[0].resource_record_name).

Two rules that prevent most bugs

1. Never treat an output as a plain value. Printing it, concatenating it with +, using it in an if, or formatting it with an f-string outside apply gives you the wrapper object, not the value. Python’s Output warns when converted to a string, and the result ends up in resource names and tags looking like garbage. Use apply, format, concat or json_dumps.

2. Don’t create resources inside apply or all. Pulumi’s documentation says to avoid it wherever possible. Resources created in a callback don’t appear in pulumi preview unless the value is already known, so the preview stops matching what up will do. Reviewers then approve a plan that leaves things out. If the number of resources depends on a value, read that value from configuration (known at preview), not from another resource’s output.

Resource identity

Wiring resources means passing some identifier, and the inputs differ in which one they want. A resource has four identities: its logical name (the first constructor argument), its physical name at the provider, its physical ID (resource.id), and its Pulumi URN. Many inputs want an ARN, an endpoint or a name instead of the ID. A type mismatch at preview usually means you passed the wrong identity. Check the input’s documentation.

Diagnosis

Symptom Cause Fix
A name or tag contains text about calling __str__ on an Output Output used in an f-string or str() Output.format/concat, or build it in apply
Preview shows fewer resources than up creates Resources created inside apply Drive counts from config. Create resources at top level
Resources created in the wrong order The dependency is not carried by any value Pass the output as an input, or depends_on as a last resort
Type error passing bucket where a string is expected Passed the resource, not a property bucket.id, bucket.arn or bucket.bucket, per the input’s docs
JSON policy has a placeholder where an ARN belongs json.dumps on a structure containing outputs Output.json_dumps

Practice

  1. Build an output URL https://<dns>/health from a load balancer’s dns_name three ways: apply, Output.format and Output.concat. Which reads best?
  2. A colleague loops for i in range(int(count_output)). Explain why this fails and rewrite it to use config.
  3. Write an S3 bucket policy that references two buckets’ ARNs, using only Output.json_dumps.

Recall card

  • Inputs accept plain values or outputs. Outputs are future values reported by the provider.
  • Output to input means a dependency edge. Ordering for create and destroy is automatic.
  • apply handles one output, Output.all several, and format/concat/json_dumps/json_loads are the common shortcuts. Lifting reads nested fields.
  • Never treat an output as plain, and never create resources inside apply.

Sources