Authoring a Source-Based Plugin Package
A source-based plugin package is a Pulumi plugin package distributed as source code rather than a pre-built executable. When a consumer runs pulumi package add against your repository or a local path, Pulumi introspects the package and generates an SDK in the consumer’s language.
Source-based plugin packages are one of three ways to distribute a component. For the comparison against native language packages and executable-based plugin packages, see Packaging Components.
pulumi-go-provider, a source-based package can also expose custom resources and functions (invokes); see Package contents by authoring language below.The key wins of the source-based model are:
- Author in your language of choice, consume in any Pulumi language. You write the package once in TypeScript, Python, Go, C#, or Java; consumers can use it from any supported Pulumi language, including YAML.
- No pre-publishing required. You don’t have to build, version, and push per-language SDKs to npm, PyPI, NuGet, Maven, and Go module proxies — a git tag (or even a local path) is enough for consumers to use the package.
pulumi package add time.This guide covers how to author a source-based plugin package: the minimum layout, an end-to-end walkthrough, per-language discovery rules, and per-language features.
For how consumers use the package in their programs, see the Components concept page. For writing the component code itself, see Build a Component.
Minimum plugin layout
Every source-based plugin package has four ingredients:
- A
PulumiPlugin.yamlmanifest declaring the authoring language. - A language-specific project manifest (
package.json,pyproject.toml,go.mod,.csproj, orpom.xml). - An entry file that the language host launches.
- One or more
ComponentResourcesubclasses.
A single plugin package can expose multiple components; you do not need one package per component. The discovery mechanism described below picks up every component in the package.
Walkthrough
This walkthrough shows the packaging flow end-to-end. The component implementation itself is intentionally left as a stub — for how to write a component, see Build a Component.
1. Create the plugin directory
mkdir my-components && cd my-components
2. Write PulumiPlugin.yaml
runtime: nodejs
runtime: python
runtime: go
runtime: dotnet
runtime: java
See the PulumiPlugin.yaml reference for all supported fields.
3. Assemble the plugin
Below is the minimum directory layout for a plugin containing one component. The shape of the component class itself is up to you — refer to Build a Component for authoring guidance. You can add more components to the same package without changing the entry file; they’ll be picked up automatically (or via explicit registration, in Go).
my-components/
├── PulumiPlugin.yaml
├── package.json # the name field determines the generated SDK name
├── tsconfig.json
└── index.ts # entry file — exports your components
index.ts:
import * as pulumi from "@pulumi/pulumi";
export class MyComponent extends pulumi.ComponentResource {
constructor(name: string, opts?: pulumi.ComponentResourceOptions) {
super("my-components:index:MyComponent", name, {}, opts);
this.registerOutputs({});
}
}
Exporting the class from the module named by main in package.json is all that’s required — Pulumi auto-discovers it.
my-components/
├── PulumiPlugin.yaml
├── pyproject.toml # the name field determines the generated SDK name
├── __main__.py # entry file — imports your components as top-level names
└── my_components/
└── components.py # your ComponentResource classes
__main__.py:
from my_components.components import MyComponent
Importing the class so it becomes a top-level name on the entry module is all that’s required — Pulumi auto-discovers it.
my-components/
├── PulumiPlugin.yaml
├── go.mod
├── main.go # entry file — registers components via pulumi-go-provider
└── provider/
└── my_component.go # your ComponentResource struct
main.go:
package main
import (
"context"
"log"
"github.com/pulumi/pulumi-go-provider/infer"
"mymodule/provider"
)
func main() {
prov, err := infer.NewProviderBuilder().
WithNamespace("my-org").
WithComponents(infer.ComponentF(provider.NewMyComponent)).
Build()
if err != nil {
log.Fatal(err)
}
_ = prov.Run(context.Background(), "my-components", "v0.0.1")
}
The generated SDK name is the string passed as the first argument to prov.Run ("my-components" above). Each component must be listed explicitly in WithComponents — Go does not auto-discover.
my-components/
├── PulumiPlugin.yaml
├── my-components.csproj # the assembly name determines the generated SDK name
├── Program.cs # entry file — hands control to ComponentProviderHost
└── MyComponent.cs # your ComponentResource class
Program.cs:
using System.Threading.Tasks;
using Pulumi.Experimental.Provider;
class Program
{
public static Task Main(string[] args) => ComponentProviderHost.Serve(args);
}
ComponentProviderHost.Serve scans the calling assembly and exposes every ComponentResource subclass it finds.
my-components/
├── PulumiPlugin.yaml
├── pom.xml # the artifactId determines the generated SDK name
└── src/main/java/com/example/provider/
├── App.java # entry file — hands the host a package to scan
└── MyComponent.java # your ComponentResource class
App.java:
package com.example.provider;
import java.io.IOException;
import com.pulumi.provider.internal.ComponentProviderHost;
public class App {
public static void main(String[] args) throws IOException, InterruptedException {
new ComponentProviderHost("my-components", App.class.getPackage()).start(args);
}
}
The generated SDK name is the first argument to the ComponentProviderHost constructor ("my-components" above). Every ComponentResource subclass in the supplied package is exposed automatically.
4. Consume the package from another project
Create a separate Pulumi program — in any supported language, not just the one the plugin was authored in — and add the package by local path:
cd ..
pulumi new typescript -n consumer --yes
cd consumer
pulumi package add ../my-components
Pulumi reads PulumiPlugin.yaml, introspects the plugin, generates a local SDK in the consumer’s language, and prints the import statement to use. Every component in the package is available under the generated SDK.
To distribute the package more broadly, push it to a Git repository and tag a release; consumers can then run pulumi package add <repo-url>@<tag>. See Distribution below for all available options.
Per-language authoring
Each language has its own rules for what the entry file must contain, how components are exposed to consumers, and how argument types are inferred for the generated SDK. These differences are a direct consequence of how each language is designed.
Two models are used across the supported languages:
- Automatic component discovery scans the plugin at load time and exposes every
ComponentResourcesubclass it finds. Depending on the language you may still need a small entry file that hands the provider host an assembly or package to scan, but no per-component registration is required — adding a new component to the package is enough to publish it. - Explicit component registration requires the author to list each component in the entry file. Only listed components are exposed.
Automatic component discovery
Export your component classes from the module named by main in your package.json. No registration code is required. A minimum plugin consists of PulumiPlugin.yaml, package.json, tsconfig.json, and an entry file like:
import * as pulumi from "@pulumi/pulumi";
export interface MyComponentArgs {
message: pulumi.Input<string>;
}
export class MyComponent extends pulumi.ComponentResource {
public readonly output: pulumi.Output<string>;
constructor(name: string, args: MyComponentArgs, opts?: pulumi.ComponentResourceOptions) {
super("my-package:index:MyComponent", name, args, opts);
this.output = pulumi.output(args.message);
this.registerOutputs({ output: this.output });
}
}
Under the hood, the pulumi-language-nodejs host recursively walks the entry module’s exports and collects every value whose prototype chain reaches ComponentResource. Nested objects and re-exported namespaces are traversed; non-component exports are ignored.
Your args types also appear in the generated SDK, so consumers can construct them in their own language. You don’t need to export or register args classes separately — Pulumi finds them by reading the second parameter of each component’s constructor using the TypeScript compiler API, then follows the property types (and any types they reference) through imports to build the schema. No decorators or base class are required on the args type.
Make your ComponentResource subclasses top-level names in your entry module. A minimum __main__.py is just:
from my_package.components import StaticSite, Database
Under the hood, the pulumi-language-python host imports the entry module and iterates over its __dict__, collecting every class that is a subclass of ComponentResource. Only top-level names are scanned — classes imported but not re-exported at module level are skipped.
Your args types also appear in the generated SDK, so consumers can construct them in their own language. You don’t need to export or register args classes separately — Pulumi finds them by reading the type annotation on each component’s __init__ args parameter. Each field of the args class becomes a property on the generated schema; Optional[...] fields become optional. The args class must be type-annotated on args and importable from the module being analyzed. No decorators are required.
Not supported. Go lacks the runtime package introspection that the other languages rely on for scanning, so every component must be registered explicitly.
Call ComponentProviderHost.Serve from Program.cs. Every ComponentResource subclass in your assembly is exposed automatically — no per-component registration is needed.
using System.Threading.Tasks;
using Pulumi.Experimental.Provider;
class Program
{
public static Task Main(string[] args) => ComponentProviderHost.Serve(args);
}
Under the hood, ComponentProviderHost.Serve scans the calling assembly via reflection and collects every non-abstract type that inherits from ComponentResource, regardless of namespace or visibility. Passing a type that does not inherit from ComponentResource causes startup to fail with a clear error.
Your args types also appear in the generated SDK, so consumers can construct them in their own language. You don’t need to register args classes separately — Pulumi finds them by looking for a constructor with exactly three parameters (string, a ResourceArgs subclass, and ComponentResourceOptions) and using the second parameter’s type. The args class is analyzed via reflection: public fields and properties become schema properties. Nullable CLR types and [Input(IsRequired = false)] mark properties optional.
Instantiate ComponentProviderHost with the package that contains your components and call start. Every ComponentResource subclass in that package (and its subpackages) is exposed automatically.
package com.example.provider;
import java.io.IOException;
import com.pulumi.provider.internal.ComponentProviderHost;
public class App {
public static void main(String[] args) throws IOException, InterruptedException {
new ComponentProviderHost("my-package", App.class.getPackage()).start(args);
}
}
Under the hood, the runtime uses the Reflections library to scan the supplied package and its subpackages, collecting every class that extends ComponentResource.
Your args types also appear in the generated SDK, so consumers can construct them in their own language. You don’t need to register args classes separately — Pulumi finds them by looking for a single constructor that takes String, a ResourceArgs subclass, and ComponentResourceOptions, and using the second parameter’s type. The args class is inspected via reflection: public fields become schema properties. Optional<T> fields and @Import(required = false) mark properties optional.
Explicit component registration
Supported but rarely needed — to hide a component from consumers, don’t export it from the entry module. If you want the explicit model anyway (for example, to expose a subset of exported classes), call componentProviderHost yourself and pass the component classes you want:
import { componentProviderHost } from "@pulumi/pulumi/provider/experimental";
import { StaticSite } from "./static-site";
import { Database } from "./database";
componentProviderHost({ components: [StaticSite, Database] });
Supported. Call component_provider_host yourself and pass the list of components you want to expose:
from pulumi.provider.experimental import component_provider_host
from my_package import StaticSite, Database
if __name__ == "__main__":
component_provider_host([StaticSite, Database], "my-package", version="1.0.0")
Go plugins are built using pulumi-go-provider. Each component must be passed to the provider builder’s WithComponents method. Only components that are explicitly registered are exposed in the generated SDK.
package main
import (
"context"
"log"
"github.com/pulumi/pulumi-go-provider/infer"
"mymodule/provider"
)
func main() {
prov, err := infer.NewProviderBuilder().
WithNamespace("my-org").
WithComponents(
infer.ComponentF(provider.NewStaticSite),
infer.ComponentF(provider.NewDatabase),
).
Build()
if err != nil {
log.Fatal(err)
}
_ = prov.Run(context.Background(), "my-components", "v0.0.1")
}
Args struct you declare on each component and mapping each field — using the pulumi:"..." struct tags, including the ,optional marker — into the generated schema.Not supported. The .NET provider host always scans an assembly for component types; there is no public API for passing an explicit list of types. To narrow the scan, place components in their own assembly and pass that assembly to ComponentProviderHost.Serve.
Not supported. The Java provider host always scans a package for component types; there is no public API for passing an explicit list of classes. To narrow the scan, place components in their own package and pass that package to the ComponentProviderHost constructor.
Consumer runtime requirements
The runtime requirements on the consumer’s machine are the same as any Pulumi program written in the authoring language.
See the JavaScript/TypeScript language docs.
See the Python language docs.
See the Go language docs.
See the .NET language docs.
See the Java language docs.
Package contents by authoring language
Source-based plugins were designed around ComponentResource. Custom resources (direct CRUD against an external API) and functions (invokes) are only supported when authoring in Go via pulumi-go-provider. In every other language today, anything that isn’t a ComponentResource is either rejected or silently ignored.
| Language | ComponentResource | CustomResource | Functions (invokes) |
|---|---|---|---|
| TypeScript | Supported | Silently ignored | Unsupported |
| Python | Supported | Silently ignored | Unsupported |
| C# / .NET | Supported | Rejected at startup | Unsupported |
| Java | Supported | Silently filtered | Unsupported |
| Go | Supported (WithComponents) | Supported (WithResources) | Supported (WithFunctions) |
ComponentResource is passed where a component is expected, rather than silently ignoring it. See pulumi/pulumi#22616 (TypeScript) and pulumi/pulumi#22617 (Python).Patterns shared by all languages
Despite the language-specific mechanics, every source-based plugin follows the same four patterns:
- Args are discovered transitively. You never register an args class separately — it is found by analyzing the component’s constructor or struct signature.
- Schema is inferred from type information. No explicit schema declaration is needed. Python reads annotations, C# and Java use reflection, TypeScript uses the compiler API, and Go uses struct tags.
- Decorators are optional. Go uses struct tags for metadata; C# and Java offer
[Input]/@Importfor fine-grained control but do not require them. - All-or-nothing export. Once a component is discoverable, every public property of its args class is automatically included in the generated schema.
Distribution
Once your plugin package is working locally, you have three ways to make it available to consumers.
Sharing via Git
Storing your package in a Git repository allows for version control, collaboration, and easier integration into multiple projects. Consumers add the package to their programs with:
pulumi package add <repo_url>[/path/to/component]@<release-version>
The only steps to enable this are pushing your package to a Git repo and tagging a release. Pulumi supports both GitHub and GitLab releases, and can also reference self-hosted Git servers — in that case omit the <release-version> portion.
Pulumi generates the consumer’s language-specific SDK automatically. For example, if the consuming program is Python, pulumi package add detects that, generates the Python SDK on-the-fly, adds the dependency to requirements.txt, and runs pip install -r requirements.txt. The output also prints the correct import statement:
$ pulumi package add https://github.com/pulumi/staticpagecomponent@v0.1.0
Downloading provider: github.com_pulumi_staticpagecomponent.git
Successfully generated a Python SDK for the staticpagecomponent package at /example/use-static-page-component/sdks/staticpagecomponent
[...]
You can then import the SDK in your Python code with:
import pulumi_static_page_component as static_page_component
GITHUB_TOKEN and GITLAB_TOKEN to authenticate access during pulumi package add.Publishing to the Pulumi IDP Private Registry
A source-based plugin package can be published to the IDP Private Registry so it shows up alongside the rest of your organization’s infrastructure building blocks — the same components and templates that power golden path workflows in Pulumi. See the Pulumi Private Registry guide for publishing instructions.
Pre-publishing language SDKs
By default, each consumer generates the SDK locally at pulumi package add time. You can instead pre-publish SDKs to language registries (npm, PyPI, Maven, NuGet, Go module proxies) so consumers install them like any other dependency. A typical CI/CD pipeline for one language looks like:
git tag v1.0.0
git push origin v1.0.0
pulumi package publish github.com/myorg/my-component@1.0.0
pulumi package gen-sdk . --language nodejs --out sdk/nodejs
npm publish
Repeat the pulumi package gen-sdk and publish steps for each language you want to support. Consumers then install the SDK directly via their package manager (e.g., npm install my-component) without generating it themselves.
Next steps
- PulumiPlugin.yaml reference — full field reference for the plugin manifest.
- Local Packages — develop and iterate on a package before publishing.
- Publishing Packages — distribute your package via Git, the IDP Private Registry, or language-specific registries.
- Package Schema — the schema format that drives SDK generation.
- Components — how consumers pull your package into their programs.
Thank you for your feedback!
If you have a question about how to use Pulumi, reach out in Community Slack.
Open an issue on GitHub to report a problem or suggest an improvement.