Pulumi Component
Guide for authoring Pulumi ComponentResource classes. Use when creating reusable infrastructure components, designing component interfaces, setting up multi-language support, or distributing component packages.
- Skill ID
- pulumi/agent-skills/pulumi-component
- Publisher
- pulumi
- Repository
- agent-skills
- Installs
- 0
- Files
- 3
- Synced
- Sep 23, 2026
Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.
pulumi/agent-skills/pulumi-componentInstalls these files- SKILL.md
- agents/openai.yaml
- use_cases.yaml
What this skill tells the agent
Authoring Pulumi Components
A ComponentResource groups related infrastructure resources into a reusable, logical unit. Components make infrastructure easier to understand, reuse, and maintain. Components appear as a single node with children nested underneath in pulumi preview/pulumi up output and in the Pulumi Cloud console.
This skill covers the full component authoring lifecycle. For general Pulumi coding patterns (Output handling, secrets, aliases, preview workflows), use the pulumi-best-practices skill instead.
When to Use This Skill
Invoke this skill when:
- Creating a new ComponentResource class
- Designing the args interface for a component
- Making a component consumable from multiple Pulumi languages
- Publishing or distributing a component package
- Refactoring inline resources into a reusable component
- Debugging component behavior (missing outputs, stuck creating, children at wrong level)
Component Anatomy
Every component has four required elements:
- Extend ComponentResource and call
super()with a type URN - Accept standard parameters: name, args, and
ComponentResourceOptions - Set `parent: this` on all child resources
- Call `registerOutputs()` at the end of the constructor
TypeScript
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
interface StaticSiteArgs {
indexDocument?: pulumi.Input<string>;
errorDocument?: pulumi.Input<string>;
}
class StaticSite extends pulumi.ComponentResource {
public readonly bucketName: pulumi.Output<string>;
public readonly websiteUrl: pulumi.Output<string>;
constructor(name: string, args: StaticSiteArgs, opts?: pulumi.ComponentResourceOptions) {
// 1. Call super with type URN: <package>:<module>:<type>
super("myorg:index:StaticSite", name, {}, opts);
// 2. Create child resources with parent: this
const bucket = new aws.s3.Bucket(`${name}-bucket`, {}, { parent: this });
const website = new aws.s3.BucketWebsiteConfigurationV2(`${name}-website`, {
bucket: bucket.id,
indexDocument: { suffix: args.indexDocument ?? "index.html" },
errorDocument: { key: args.errorDocument ?? "error.html" },
}, { parent: this });
// 3. Expose outputs as class properties
this.bucketName = bucket.id;
this.websiteUrl = website.websiteEndpoint;
// 4. Register outputs -- always the last line
this.registerOutputs({
bucketName: this.bucketName,
websiteUrl: this.websiteUrl,
});
}
}
// Usage
const site = new StaticSite("marketing", {
indexDocument: "index.html",
});
export const url = site.websiteUrl;Python
import pulumi
import pulumi_aws as aws
class StaticSiteArgs:
def __init__(self,
index_document: pulumi.Input[str] = "index.html",
error_document: pulumi.Input[str] = "error.html"):
self.index_document = index_document
self.error_document = error_document
class StaticSite(pulumi.ComponentResource):
bucket_name: pulumi.Output[str]
website_url: pulumi.Output[str]
def __init__(self, name: str, args: StaticSiteArgs,
opts: pulumi.ResourceOptions = None):
super().__init__("myorg:index:StaticSite", name, None, opts)
bucket = aws.s3.Bucket(f"{name}-bucket",
opts=pulumi.ResourceOptions(parent=self))
website = aws.s3.BucketWebsiteConfigurationV2(f"{name}-website",
bucket=bucket.id,
index_document=aws.s3.BucketWebsiteConfigurationV2IndexDocumentArgs(
suffix=args.index_document,
),
error_document=aws.s3.BucketWebsiteConfigurationV2ErrorDocumentArgs(
key=args.error_document,
),
opts=pulumi.ResourceOptions(parent=self))
self.bucket_name = bucket.id
self.website_url = website.website_endpoint
self.register_outputs({
"bucket_name": self.bucket_name,
"website_url": self.website_url,
})
site = StaticSite("marketing", StaticSiteArgs())
pulumi.export("url", site.website_url)Type URN Format
The first argument to super() is the type URN: <package>:<module>:<type>.
| Segment | Convention | Example |
|---|---|---|
| package | Organization or package name | myorg, acme, pkg |
| module | Usually index | index |
| type | PascalCase class name | StaticSite, VpcNetwork |
Full examples: myorg:index:StaticSite, acme:index:KubernetesCluster
registerOutputs Is Required
Why: Without registerOutputs(), the component appears stuck in a "creating" state in the Pulumi console and outputs are not persisted to state.
Wrong:
class MyComponent extends pulumi.ComponentResource {
public readonly url: pulumi.Output<string>;
constructor(name: string, args: MyArgs, opts?: pulumi.ComponentResourceOptions) {
super("myorg:index:MyComponent", name, {}, opts);
const bucket = new aws.s3.Bucket(`${name}-bucket`, {}, { parent: this });
this.url = bucket.bucketRegionalDomainName;
// Missing registerOutputs -- component stuck "creating"
}
}Right:
class MyComponent extends pulumi.ComponentResource {
public readonly url: pulumi.Output<string>;
constructor(name: string, args: MyArgs, opts?: pulumi.ComponentResourceOptions) {
super("myorg:index:MyComponent", name, {}, opts);
const bucket = new aws.s3.Bucket(`${name}-bucket`, {}, { parent: this });
this.url = bucket.bucketRegionalDomainName;
this.registerOutputs({ url: this.url });
}
}Derive Child Names from the Component Name
Why: Hardcoded child names cause collisions when the component is instantiated multiple times.
Wrong:
// Collides if two instances of this component exist
const bucket = new aws.s3.Bucket("my-bucket", {}, { parent: this });Right:
// Unique per component instance
const bucket = new aws.s3.Bucket(`${name}-bucket`, {}, { parent: this });Designing the Args Interface
The args interface is the most impactful design decision. It defines what consumers can configure and how composable the component is.
Wrap Properties in Input<T>
Why: Input<T> accepts both plain values and Output<T> from other resources. Without it, consumers must unwrap outputs manually with .apply().
Wrong:
interface WebServiceArgs {
port: number; // Forces consumers to unwrap Outputs
vpcId: string; // Cannot accept vpc.id directly
}Right:
interface WebServiceArgs {
port: pulumi.Input<number>; // Accepts 8080 or someOutput
vpcId: pulumi.Input<string>; // Accepts "vpc-123" or vpc.id
}Keep Structures Flat
Avoid deeply nested arg objects. Flat interfaces are easier to use and evolve.
// Prefer flat
interface DatabaseArgs {
instanceClass: pulumi.Input<string>;
storageGb: pulumi.Input<number>;
enableBackups?: pulumi.Input<boolean>;
backupRetentionDays?: pulumi.Input<number>;
}
// Avoid deep nesting
interface DatabaseArgs {
instance: {
compute: { class: pulumi.Input<string> };
storage: { sizeGb: pulumi.Input<number> };
};
backup: {
config: { enabled: pulumi.Input<boolean>; retention: pulumi.Input<number> };
};
}No Union Types
Union types break multi-language SDK generation. Python, Go, and C# cannot represent string | number.
Wrong:
interface MyArgs {
port: pulumi.Input<string | number>; // Fails in Python, Go, C#
}Right:
interface MyArgs {
port: pulumi.Input<number>; // Single type, works everywhere
}More from pulumi
Use this skill in a project
Open any RiverX project, search the Skills panel for pulumi/agent-skills/pulumi-component, and install it in one click.
