Configuring the Entity Scaffolder Content Frontend Plugin#
This guide covers the configuration options available for the Entity Scaffolder Content frontend plugin.
New Frontend System Configuration (Alpha)#
When using the new frontend system through the /alpha export, the plugin is configured automatically with sensible defaults. The configuration options described below are still available and can be customised through direct component usage.
In the new frontend system, field extensions are discovered automatically — any extension registered via FormFieldBlueprint (e.g. RepoUrlPicker, EntityPicker) is available without any additional wiring.
Component Configuration#
EntityScaffolderContent Props#
The main component accepts the following configuration props:
interface EntityScaffolderContentProps {
// Define template filtering and grouping
templateGroupFilters: Array<{
title?: ReactNode;
filter: (entity: Entity, template: TemplateEntityV1beta3) => boolean;
}>;
// Map entity and template data to template form fields
buildInitialState: (
entity: Entity,
template: TemplateEntityV1beta3,
) => Record<string, JsonValue>;
// Optional explicit field extensions.
// Not needed in the new frontend system — extensions are auto-discovered.
ScaffolderFieldExtensions?: ReactNode;
// Optional custom layout options for the workflow stepper
layouts?: LayoutOptions[];
// Optional component overrides
components?: {
TemplateCard?: ComponentType<{ template: TemplateEntityV1beta3 }>;
};
}
Note: The
buildInitialStatefunction now receives both theentityand the selectedtemplateas arguments. This allows you to tailor initial values per template.
Template Group Filters#
Configure how templates are filtered and grouped based on entity context:
const templateGroupFilters = [
{
title: 'Kubernetes Resources',
filter: (entity, template) =>
template.metadata?.labels?.type === 'kubernetes' &&
entity.spec?.type === 'kubernetes-namespace',
},
{
title: 'Application Templates',
filter: (entity, template) =>
template.metadata?.labels?.type === 'application' &&
entity.spec?.type === 'service',
},
];
Initial State Builder#
Define how entity data maps to template form fields. The function now receives both entity and template:
const buildInitialState = (entity: Entity, template: TemplateEntityV1beta3) => ({
// Basic metadata mapping
name: entity.metadata.name,
namespace: entity.metadata.namespace,
// Extract from annotations
cluster: entity.metadata?.annotations?.['backstage.io/managed-by-location']?.split(': ')[1],
// Custom transformations
labels: Object.entries(entity.metadata.labels || {}).map(
([key, value]) => `${key}=${value}`
),
});
Field Extensions#
New Frontend System (recommended)#
In the new frontend system, field extensions registered via FormFieldBlueprint are automatically discovered — no ScaffolderFieldExtensions prop is needed.
Legacy Frontend System#
If you are using the legacy frontend system, pass extensions explicitly via the ScaffolderFieldExtensions prop:
import { ScaffolderFieldExtensions } from '@backstage/plugin-scaffolder-react';
import { EntityPickerFieldExtension, RepoUrlPickerFieldExtension } from '@backstage/plugin-scaffolder';
<EntityScaffolderContent
templateGroupFilters={templateGroupFilters}
buildInitialState={buildInitialState}
ScaffolderFieldExtensions={
<ScaffolderFieldExtensions>
<RepoUrlPickerFieldExtension />
<EntityPickerFieldExtension />
</ScaffolderFieldExtensions>
}
/>
The
ScaffolderFieldExtensionsprop is still accepted in the new frontend system and will be merged with auto-discovered extensions, so existing usage continues to work.
Custom Layouts#
Pass custom layout options to the workflow stepper:
import { LayoutOptions } from '@backstage/plugin-scaffolder-react';
const layouts: LayoutOptions[] = [
{
name: 'TwoColumn',
component: TwoColumnLayout,
},
];
<EntityScaffolderContent
templateGroupFilters={templateGroupFilters}
buildInitialState={buildInitialState}
layouts={layouts}
/>
Custom Template Card#
Override the default template card component:
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
const MyTemplateCard = ({ template }: { template: TemplateEntityV1beta3 }) => (
<div>
<h3>{template.metadata.title ?? template.metadata.name}</h3>
<p>{template.metadata.description}</p>
</div>
);
<EntityScaffolderContent
templateGroupFilters={templateGroupFilters}
buildInitialState={buildInitialState}
components={{ TemplateCard: MyTemplateCard }}
/>
Form Decorators#
Form decorators allow you to collect secrets or modify form state before the scaffold call is made. A common use-case is fetching a GitHub OAuth token to prevent self-approval of Backstage-created PRs.
How They Work#
- A template declares the decorators it needs in
spec.formDecorators:
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: my-template
spec:
# ...
formDecorators:
- id: github-token
-
When the user submits the form, the plugin automatically runs all declared decorators in order before calling the scaffolder backend.
-
Decorators can set secrets (passed to the backend but not stored in the task) and/or update form state values.
Registering a Decorator (New Frontend System)#
import { createFrontendModule } from '@backstage/frontend-plugin-api';
import {
createScaffolderFormDecorator,
FormDecoratorBlueprint,
} from '@backstage/plugin-scaffolder-react/alpha';
import { githubAuthApiRef } from '@backstage/core-plugin-api';
const githubTokenDecorator = createScaffolderFormDecorator({
id: 'github-token',
deps: {
githubApi: githubAuthApiRef,
},
decorator: async ({ setSecrets, secrets }, { githubApi }) => {
const token = await githubApi.getAccessToken(['repo']);
// Functional update — spread existing secrets and add the new one
setSecrets(prev => ({ ...prev, GITHUB_TOKEN: token }));
},
});
export const githubTokenDecoratorExtension = FormDecoratorBlueprint.make({
name: 'github-token',
params: {
decorator: githubTokenDecorator,
},
});
// Register in App.tsx
createFrontendModule({
pluginId: 'scaffolder',
extensions: [githubTokenDecoratorExtension],
});
Decorator Context#
The decorator function receives:
| Property | Type | Description |
|---|---|---|
input |
Record<string, JsonValue> |
Values from the template's spec.formDecorators[].input |
formState |
Record<string, JsonValue> |
Current form field values |
setFormState |
(state \| (prev => state)) => void |
Update form field values |
secrets |
Record<string, string> |
Current secrets |
setSecrets |
(state \| (prev => state)) => void |
Update secrets (supports functional updates) |
Both setFormState and setSecrets accept either a plain object (replaces the entire state) or a functional update prev => next (use this when you want to merge with existing values).
Entity Page Integration#
Basic Integration#
Add the plugin to an entity page:
import { EntityScaffolderContent } from '@terasky/backstage-plugin-entity-scaffolder-content';
const entityPage = (
<EntityLayout>
<EntityLayout.Route
path="/scaffolder"
title="Templates"
>
<EntityScaffolderContent
templateGroupFilters={templateGroupFilters}
buildInitialState={buildInitialState}
/>
</EntityLayout.Route>
</EntityLayout>
);
With Explicit Field Extensions (Legacy)#
import { EntityScaffolderContent } from '@terasky/backstage-plugin-entity-scaffolder-content';
import { GitOpsManifestUpdaterExtension } from '@terasky/backstage-plugin-gitops-manifest-updater';
import { ScaffolderFieldExtensions } from '@backstage/plugin-scaffolder-react';
import { EntityPickerFieldExtension, RepoUrlPickerFieldExtension } from '@backstage/plugin-scaffolder';
const entityPage = (
<EntityLayout>
<EntityLayout.Route
path="/scaffolder"
title="Templates"
>
<EntityScaffolderContent
templateGroupFilters={templateGroupFilters}
buildInitialState={buildInitialState}
ScaffolderFieldExtensions={
<ScaffolderFieldExtensions>
<RepoUrlPickerFieldExtension />
<EntityPickerFieldExtension />
<GitOpsManifestUpdaterExtension />
</ScaffolderFieldExtensions>
}
/>
</EntityLayout.Route>
</EntityLayout>
);
Advanced Integration#
Configure for multiple entity types:
const serviceEntityPage = (
<EntityLayout>
<EntityLayout.Route
path="/scaffolder"
title="Service Templates"
>
<EntityScaffolderContent
templateGroupFilters={[
{
title: 'Service Templates',
filter: (entity, template) =>
template.metadata?.labels?.type === 'service' &&
entity.spec?.type === 'service',
},
]}
buildInitialState={(entity, template) => ({
serviceName: entity.metadata.name,
owner: entity.spec?.owner,
type: entity.spec?.type,
})}
/>
</EntityLayout.Route>
</EntityLayout>
);
const systemEntityPage = (
<EntityLayout>
<EntityLayout.Route
path="/scaffolder"
title="System Templates"
>
<EntityScaffolderContent
templateGroupFilters={[
{
title: 'System Resources',
filter: (entity, template) =>
template.metadata?.labels?.type === 'system' &&
entity.spec?.type === 'system',
},
]}
buildInitialState={(entity, template) => ({
systemName: entity.metadata.name,
environment: entity.spec?.environment,
})}
/>
</EntityLayout.Route>
</EntityLayout>
);
Best Practices#
-
Template Filtering
- Use clear, descriptive group titles
- Keep filter conditions simple and maintainable
- Consider template metadata structure
- Handle edge cases gracefully
-
Data Mapping
- Validate entity data before mapping
- Provide sensible defaults
- Use the
templateargument inbuildInitialStateto conditionally populate fields - Handle missing data gracefully
-
Form Decorators
- Use decorators to collect sensitive data (tokens, credentials) that should not be stored in the task
- Always use functional updates (
prev => ({ ...prev, key: value })) when adding to existing state/secrets - Keep decorator logic focused — one concern per decorator
-
Entity Integration
- Use consistent route paths
- Group related templates logically
- Consider user workflow
- Maintain clear navigation
For installation instructions, refer to the Installation Guide.