When to use this skill
You're authoring a frontend plugin and want to give other plugins or modules a typed, structured way to contribute UI to it. Think:
- A dashboards plugin that wants modules to register custom dashboard panels
- A code-quality plugin that wants modules to register additional checks shown on a results page
- A reusable "feature flag" plugin where modules contribute UI controls per flag
This is the same pattern Backstage itself uses for PageBlueprint, EntityCardBlueprint, EntityContentBlueprint, FormFieldBlueprint, ApiBlueprint. You're creating a new one of those for your plugin.
Don't create a blueprint when:
- A built-in blueprint already covers the case (use
EntityCardBlueprintfor catalog-page cards,FormFieldBlueprintfor scaffolder fields, etc.) - Your extension point only fires once and only your plugin will ever populate it — just write a regular extension and skip the blueprint indirection
- You're in a backend plugin — backend uses extension points (
createExtensionPoint), not blueprints. Seeusing-backstage-backend-system/references/extension-points.md
What you build
A blueprint comes in two coupled pieces:
- The blueprint itself (
createExtensionBlueprint) — the typed factory other plugins call asMyBlueprint.make({ name, params })to contribute a single instance. - A parent extension that declares an
inputand renders or uses everything attached to it. This is usually aPageBlueprintextension you define in your plugin'sextensions.tsx.
Both pieces are required and must agree on the input name and the data refs the contributions emit. Most plugins author both in the same extensions.tsx (or a colocated module).
Choose the right pattern
| Building... | Reference |
|---|---|
The blueprint itself (createExtensionBlueprint definition) |
references/defining-the-blueprint.md |
The parent extension that collects contributions via inputs |
references/parent-collecting-extensions.md |
Two invocation modes
- Standalone: editing an existing frontend plugin to add a new blueprint or refine an existing one.
- Post-scaffold (called by
creating-backstage-pluginforfrontend-plugin): rare — only when the user's intent explicitly describes "I want my plugin to be extensible by other modules". Most plugins don't need this and shouldn't have it.
Common gotchas
- Blueprints live on the frontend. The backend equivalent is
createExtensionPoint— different API, different package. If you want both extensible UI AND extensible backend behavior, create both, with separate symbols and separate documentation. - The
attachToof your blueprint MUST point at an input declared on a real extension in the same plugin'sextensionsarray. Mismatched id/input strings produce silent "no contributions" — the blueprint compiles, but nothing ever attaches. - Pick
outputcarefully — these are the data refs every contribution must yield. If a blueprint requirescoreExtensionData.reactElementplus a custom data ref, every consumer's factory has to yield both. Adding required refs later is a breaking change for consumers. - Use
dataRefsfor optional, named accessors the parent reads off contributions (e.g. "the contribution can optionally provide a sort key, accessed asMyBlueprint.dataRefs.sortKey"). Required output goes inoutput; optional/contribution-typed-data goes indataRefs. configSchemalets adopters override per-contribution settings via app-config without touching code. Define one only when the contribution has user-facing settings (titles, paths, filters); skip otherwise.- The
factoryis a generator —function* (...)yieldingref(value)calls. Eachyieldadds one piece of extension data. Loops, conditionals, and async-looking work all happen synchronously inside the factory; truly-async work belongs in theloadercallback the params pass. - Two-stage params (
createExtensionBlueprintParams) — for blueprints whose params need to be a function-form (e.g.{ field: () => Promise<...> }), wrap withcreateExtensionBlueprintParamsso consumers get type inference. SeeFormFieldBlueprintin../backstage/plugins/scaffolder-react/src/next/blueprints/FormFieldBlueprint.tsxfor a real example. makevsmakeWithOverrides: consumers normally callMyBlueprint.make({ name, params }). ThemakeWithOverridesvariant lets a more advanced consumer overridefactoryto wrap the original — used wheninputs:are added on top of a blueprint. Documentmakein your README; mentionmakeWithOverridesonly for advanced cases.