npx skills add ...
npx skills add pulumi/agent-skills --skill 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.
npx skills add pulumi/agent-skills --skill pulumi-component
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.
Invoke this skill when:
Every component has four required elements:
super() with a type URNComponentResourceOptionsparent: this on all child resourcesregisterOutputs() at the end of the constructorThe 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
Why: Without registerOutputs(), the component appears stuck in a "creating" state in the Pulumi console and outputs are not persisted to state.
Wrong:
Right:
Why: Hardcoded child names cause collisions when the component is instantiated multiple times.
Wrong:
Right:
The args interface is the most impactful design decision. It defines what consumers can configure and how composable the component is.
Why: Input<T> accepts both plain values and Output<T> from other resources. Without it, consumers must unwrap outputs manually with .apply().
Wrong:
Right:
Avoid deeply nested arg objects. Flat interfaces are easier to use and evolve.
Union types break multi-language SDK generation. Python, Go, and C# cannot represent string | number.
Wrong:
Right:
If you need to accept multiple forms, use separate optional properties:
Functions cannot be serialized across language boundaries.
Wrong:
Right:
Set sensible defaults inside the constructor so consumers only configure what they need:
Components often create many internal resources. Expose only the values consumers need, not every internal resource.
Wrong:
Right:
Use pulumi.interpolate or pulumi.concat to build derived values:
Encode best practices as defaults. Allow consumers to override when they have specific requirements.
Use optional args to gate creation of sub-resources:
Build higher-level components from lower-level ones. Each level manages a single concern.
Accept explicit providers for multi-region or multi-account deployments. ComponentResourceOptions carries provider configuration to children automatically:
Children with { parent: this } automatically inherit the provider. No extra code is needed inside the component.
If your component will be consumed from multiple Pulumi languages (TypeScript, Python, Go, C#, Java, YAML), package it as a multi-language component.
Ask: "Will anyone consume this component from a different language than it was authored in?"
Single-language component (no packaging needed):
PulumiPlugin.yaml needed -- just import the class directlyMulti-language component (packaging required):
Common mistake: A TypeScript platform team builds components only their TypeScript users can consume. If application developers use Python or YAML, those components are invisible to them without multi-language packaging.
Create a PulumiPlugin.yaml in the component directory to declare the runtime:
Or for Python:
For multi-language compatibility, args must be serializable. These constraints apply regardless of the authoring language:
| Allowed | Not Allowed |
|---|---|
string, number, boolean | Union types (string | number) |
Input<T> wrappers | Functions and callbacks |
| Arrays and maps of primitives | Complex nested generics |
| Enums | Platform-specific types |
Consumers install the component with pulumi package add, which automatically downloads the provider plugin, generates a local SDK in the consumer's language, and updates Pulumi.yaml:
For fresh checkouts or CI environments, run pulumi install to ensure all package dependencies are available. The consumer does not need to manually generate SDKs.
Authors who publish SDKs to package managers (npm, PyPI, etc.) can optionally use pulumi package gen-sdk to generate language-specific SDKs for publishing. Most component authors do not need this -- pulumi package add handles SDK generation on the consumer side.
Published multi-language components require an entry point that hosts the component provider process. The entry point pattern differs by language.
TypeScript (runtime: nodejs):
Export component classes from index.ts. No separate entry point file is needed. Pulumi introspects exported classes automatically.
Python (runtime: python):
Create a __main__.py that calls component_provider_host with all component classes:
Go (runtime: go):
Create a main.go that builds and runs the provider:
C# (runtime: dotnet):
Create a Program.cs that serves the component provider host:
For a complete working example across all languages, see https://github.com/mikhailshilkov/comp-as-comp.
Reference: https://www.pulumi.com/docs/iac/using-pulumi/pulumi-packages/
Choose a distribution method based on your audience:
| Audience | Method | How |
|---|---|---|
| Same project | Direct import | Standard language import |
| Same organization | Private registry | pulumi package publish to Pulumi Cloud |
| Same organization | Git repository | pulumi package add <repo> with version tags |
| Language ecosystem | Package manager | Publish to npm, PyPI, NuGet, or Maven |
| Public community | Pulumi Registry | Submit via pulumi/registry GitHub repo |
The private registry is the centralized catalog for your organization's components. It provides automatic API documentation, version management, and discoverability for all teams.
Publish a component to the private registry:
Version components using git tags with a v prefix:
A README file is required when publishing. Pulumi uses it as the component's documentation page in the registry.
Automate publishing from GitHub Actions using OIDC authentication:
Prerequisites: Configure GitHub OIDC integration with Pulumi Cloud before using this workflow.
The registry supports private GitHub and GitLab repositories. For non-OIDC setups, authenticate with GITHUB_TOKEN or GITLAB_TOKEN environment variables.
The private registry automatically generates SDK documentation for each published component. Enrich the generated docs by adding type annotations to your component's inputs and outputs (JSDoc in TypeScript, docstrings in Python, Annotate() methods in Go).
Reference: https://www.pulumi.com/docs/idp/get-started/private-registry/
Tag releases for consumers to pin versions:
Consumers install with:
Publish language-specific packages for native dependency management:
npm publish for TypeScript/JavaScripttwine upload for Pythondotnet nuget push for .NETReference: https://www.pulumi.com/docs/iac/using-pulumi/pulumi-packages/
| Anti-Pattern | Problem | Fix |
|---|---|---|
Resources inside apply() | Not visible in pulumi preview | Move resource creation outside apply (see pulumi-best-practices practice 1) |
Missing registerOutputs() | Component stuck "creating" | Always call as last line of constructor |
Missing parent: this | Children appear at root level | Pass { parent: this } to all child resources |
| Union types in args | Breaks Python, Go, C# SDKs | Use single types; separate properties for variants |
| Functions in args | Cannot serialize across languages | Use configuration properties instead |
| Hardcoded child names | Collisions with multiple instances | Derive names from ${name}-suffix |
| Over-exposed outputs | Leaks implementation details | Export only what consumers need |
| Single-use component | Unnecessary abstraction overhead | Use inline resources until a pattern repeats |
| Deeply nested args | Hard to use and evolve | Keep interfaces flat with optional properties |
| Topic | Key Point |
|---|---|
| Type URN | <package>:<module>:<type>, module usually index |
| Constructor | super(type, name, {}, opts) then children then registerOutputs() |
| Child resources | Always { parent: this }, derive name from ${name}-suffix |
| Args interface | Wrap in Input<T>, no unions, no functions, flat structure |
| Outputs | Public readonly Output<T> properties, expose only essentials |
| Defaults | Use ?? operator to apply sensible defaults in constructor |
| Composition | Lower-level components composed into higher-level ones |
| Multi-language | PulumiPlugin.yaml + entry point; consumers use pulumi package add |
| Distribution | Private registry, git tags, package managers, or public Pulumi Registry |