Install
$ agentstack add skill-bendaamerahmed-backstage-idp-plugin-backstage-scaffolder ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Backstage Scaffolder
Write Template entities and custom scaffolder actions that survive review, run under the permission framework, and fail loudly instead of silently.
Preconditions
- Release line from
backstage.json; scaffolder packages resolved viayarn why @backstage/plugin-scaffolder-backend. - Backend generation:
createBackend()+backend.add(import('@backstage/plugin-scaffolder-backend'))inpackages/backend/src/index.tsis the new backend system. Apackages/backend/src/plugins/scaffolder.tsrouter is legacy — migrate first (backstage-plugin-migrate) or register actions through the router's options and say so in your report. - Frontend generation matters only for custom field extensions: NFS uses
FormFieldBlueprint+createFormFieldfrom@backstage/plugin-scaffolder-react/alpha. Read the installed package's exports rather than assuming the legacy registration shape. @backstage/plugin-catalog-backend-module-scaffolder-entity-modelpresent in the backend — without itkind: Templatedoes not validate and no template appears.- Working SCM
integrationsinapp-config.yamlfor whichever host the template publishes to. - A local backend you may run. Any run that touches a real SCM org is an external mutation — stop and get authorization first.
Procedure
- Inventory before authoring.
GET /api/scaffolder/v2/actions(or the/create/actionspage) for installed actions and their input/output schemas;GET /api/scaffolder/v2/templating-extensionsfor available filters and globals. Never guess an action's input keys — they are published there. - Start the entity.
apiVersion: scaffolder.backstage.io/v1beta3,kind: Template,metadata.name, andspec.owner+spec.type. Body isspec.parameters,spec.steps,spec.output. - Write
spec.parametersas oneFormStepor an array of them. Each step is JSON Schema (title,description,required,properties) with rjsfui:*keys merged in —ui:autofocus,ui:emptyValue,ui:help,ui:widget,ui:options. Array elements become separate wizard pages; use them to keep any one page short. Custom validation messages followajv-errors. - Use the built-in pickers instead of free-text strings.
ui:field: RepoUrlPickerwithui:options.allowedHosts(must match anintegrationshost), plusallowedOwners/allowedReposto narrow. Value is a repo spec string likegithub.com?repo=x&owner=y, not a URL.ui:field: OwnerPickerwithui:options.catalogFilter— eitherkind: [Group, User]or a list of full catalog API filters (metadata.annotations.github.com/team-slug: { exists: true }).ui:field: EntityPickerfor arbitrary catalog entities;RepoBranchPickerandRepoOwnerPickerfor autocomplete, both of which requirerequestUserCredentials(andhostfor the owner picker) to function.- Set
ui:options.requestUserCredentials: { secretsKey: USER_OAUTH_TOKEN, additionalScopes: { github: [workflow] } }when the template must act as the user; consume it as${{ secrets.USER_OAUTH_TOKEN }}. Requires a configured auth provider andScmAuthApi(backstage-auth).
- Route every credential through secrets, never parameters.
ui:field: Secretkeeps the value out of the task record and REST responses and masks it in the review step. Read it as${{ secrets.name }}—${{ parameters.name }}will be undefined.- For programmatic task creation declare
spec.secrets.schemawithrequired/properties. A missing secret then fails task creation with400andsecrets.X is required, instead of mid-run. - Org-wide values belong in
scaffolder.defaultEnvironmentinapp-config.yaml, read as${{ environment.parameters.* }}and${{ environment.secrets.* }}. Environment secrets are masked in logs and never reach the frontend.
- Write
spec.stepsasid,name,action,input, with optionalifandeach. Use camelCase step and action ids: a dash makes${{ steps.my-action.output.x }}evaluate toNaN, and the bracket form${{ steps['my-action'].output.x }}is the only workaround. Witheach, the iteration value is${{ each.value }}(or${{ each.value.field }}), and the step's outputs become an array. - Prefer built-ins over custom code.
- Shipped in
@backstage/plugin-scaffolder-backend:fetch:plain,fetch:plain:file,fetch:template,fetch:template:file,catalog:register,catalog:write,debug:log,debug:wait,fs:delete,fs:rename,fs:readdir. - Publish/PR actions come from
@backstage/plugin-scaffolder-backend-module-{github,gitlab,azure,bitbucket-cloud,bitbucket-server,gerrit,gitea}; add one withyarn --cwd packages/backend addthenbackend.add(import('')). - Community actions live under
@backstage-community/plugin-scaffolder-backend-module-*. Read the handler before installing one.
- Template the skeleton with
fetch:template. Inside skeleton files the variables are${{ values.x }}— onlytemplate.yamlitself sees${{ parameters.x }}— and they must be passed explicitly throughinput.values. UsecopyWithoutTemplatingfor files whose own${{ }}syntax must survive (GitHub Actions workflows, Helm charts),targetPathto place output in a subdirectory, andreplace: trueonly when overwriting existing workspace files is intended. - Glue steps with expressions.
${{ }}is evaluated by Nunjitsu, a deliberately reduced subset of Nunjucks. Check its compatibility guide before using any Nunjucks tag or filter; do not assume full Nunjucks.- Built-in filters:
parseRepoUrl,parseEntityRef(accepts{ defaultKind, defaultNamespace }),pick('name'),projectSlug. Custom filters and globals are registered from a backend module againstscaffolderTemplatingExtensionPoint. ${{ user.entity }}gives the caller's catalogUserentity — useful forgitAuthorName/gitAuthorEmail— and requires a sign-in resolver that maps to a catalog user.
- Handle failure paths explicitly. After a step fails, later steps are skipped unless their
ifinvokes${{ always() }}or${{ failure() }}. Any template that creates external resources before a step that can fail needs afailure()cleanup step;if: ${{ true }}will not run. - Finish with
spec.output.linkstaketitleplusurl, oricon+entityReffor a catalog link;textitems taketitle+contentmarkdown. Both accept a per-itemif. Source values from${{ steps['publish'].output.remoteUrl }}/${{ steps['register'].output.entityRef }}. - Gate sensitive parameters and steps with
backstage:permissions: { tags: [] }on the parameter step or the step, then enforce in the policy (step 15).backstage:featureFlaghides parameters or fields but cannot gatespec.steps[].if— expose a boolean parameter and branch on it instead. - Scaffold custom actions, do not hand-roll.
yarn backstage-cli new→scaffolder-backend-modulegenerates the package,module.ts, an action and a test.
createTemplateActionfrom@backstage/plugin-scaffolder-nodetakesid(namespacedprovider:entity:verb, camelCase segments),description,examples,supportsDryRun,schema.input/schema.output,handler.- Current schemas are per-property zod callbacks —
contents: z => z.string({ description: '...' }). The accepted schema shape has changed across releases, so readcreateTemplateAction's type from the installed package before writing it. examples: TemplateExample[](YAML strings of astepssnippet) is what renders on/create/actions. Without it, template authors cannot discover the action's usage.
- Register the action in
createBackendModule, whose option shape you read from
the installed @backstage/backend-plugin-api types — currently pluginId, moduleId and register.
- Depend on
scaffolderActionsExtensionPointfrom@backstage/plugin-scaffolder-nodeand callscaffolder.addActions(myAction(...)). - Pass core services (
coreServices.rootConfig,coreServices.cache,coreServices.discovery,coreServices.auth) asdepsand close over them in the action factory; never reach for globals inside a handler. - Inside the handler use only
ctx:ctx.input,ctx.output(key, value),ctx.logger,ctx.workspacePath,ctx.createTemporaryDirectory(),ctx.isDryRun,ctx.metadata.name,ctx.checkpoint(experimental idempotency — version the key whenever its return type changes, or a retried task fails on the stale cached value). - Resolve every path with
resolveSafeChildPath(ctx.workspacePath, ctx.input.filename)from@backstage/backend-plugin-api.
- Enforce permissions in the policy (
backstage-permissions). Without one, every signed-in user may execute every template and every action.
- Permissions from
@backstage/plugin-scaffolder-common/alpha:templateParameterReadPermission,templateStepReadPermission,actionExecutePermission,taskCreatePermission,taskReadPermission,taskCancelPermission. - Decisions and rules from
@backstage/plugin-scaffolder-backend/alpha:createScaffolderTemplateConditionalDecision+scaffolderTemplateConditions.hasTag;createScaffolderActionConditionalDecision+scaffolderActionConditions.hasActionId/hasProperty;createScaffolderTaskConditionalDecision+scaffolderTaskConditions.isTaskOwner. - Rules compose with
not/allOf/anyOf— e.g. denydebug:logonly whenhasProperty({ key: 'message', value: 'not-this!' }).
- Register the template as a
Location—catalog.locationswithrules: [{ allow: [Template] }], or/catalog-import. Restrict which repositories may contributeTemplateentities: scaffolder jobs run on the backend host with the backend's credentials, so template authorship is a privileged capability.
Verification
yarn tscandyarn testfrom the repo root;yarn backstage-cli config:check --laxafter touchingconfig.d.ts.- Action unit tests:
createMockActionContextfrom@backstage/plugin-scaffolder-node-test-utils, with an explicitworkspacePathfromcreateMockDirectory()(@backstage/backend-test-utils) when called insideit. Assert onctx.outputcalls. Add a case withisDryRun: trueproving no external call happens. - Template iteration: Template Editor at
/create/edit→ Load Template Directory, fill the form,Createruns a dry run and opens a drawer with the resulting file tree plus per-action logs. Equivalent API:POST /api/scaffolder/v2/dry-runwith{ template, values, secrets, directoryContents }. - Form-only check:
GET /api/scaffolder/v2/templates/{namespace}/{kind}/{name}/parameter-schemareturns the rendered steps — empty or 404 means the entity is not in the catalog. - Real run:
yarn start+yarn start-backend, execute the template against a scratch org only, then confirm the created entity resolves in the catalog. If the repo does not identify a non-production target org, return a BLOCKED report instead of picking one. - Action registered: it appears in
GET /api/scaffolder/v2/actionswith its schema andexamples.
Failure modes
- Template does not appear under
/create. Check in this order, and read the entity'sstatus.itemsbefore touching the YAML — schema errors there are silent in the UI: - the
Locationwas never refreshed after the edit (refresh from the Locations view, orPOST /api/catalog/refreshwith the location'sentityRef); catalog.rulesdoes notallow: [Template]for that location;@backstage/plugin-catalog-backend-module-scaffolder-entity-modelis absent, sokind: Templatenever validates;spec.typeorspec.owneris missing, or the file still saysv1beta2.Template action with ID '' is not registered. The module is installed but notbackend.add-ed; or it was registered on a legacyscaffolder.tsrouter while the app now bootscreateBackend(); or the id differs by case or namespace./create/actionsis the source of truth, not the module's README.- A step output is
NaNor empty. Dashed step/action id read with dot notation. Rename to camelCase, or use${{ steps['id'].output.x }}. - A secret arrives
undefinedin the action. Read as${{ parameters.x }}instead of${{ secrets.x }}, orrequestUserCredentials.secretsKeydoes not match the name used in the step. Secrets are also absent on retry unless re-supplied —POST /v2/tasks/{taskId}/retryaccepts asecretsbody for exactly this. - Task sits in
open/processingforever. scaffolder.concurrentTasksLimit: 0disables task workers on that deployment entirely; otherwise every replica that could claim it is down or saturated (default limit 10).- A crashed worker leaves the task apparently alive: stale tasks are only reaped against
scaffolder.taskTimeout(default 24h) on thetaskTimeoutJanitorFrequencycycle (default 5m). EXPERIMENTAL_recoverTasks+EXPERIMENTAL_workspaceSerialization(withEXPERIMENTAL_recoverTasksTimeout) are what make restarts resumable; without them a rolling deploy strands in-flight tasks.POST /v2/tasks/{taskId}/cancelto clear one; treat it as a mutation of someone else's run.- Task failed mid-run and left real resources behind.
GET /api/scaffolder/v2/tasks/{taskId}/events(poll withafter=) is the per-step log;GET /v2/tasks/{taskId}gives status and the recorded steps;GET /v2/tasks?createdBy=&status=lists them. The repository created by an earlier step still exists — the scaffolder does not roll back. Addif: ${{ failure() }}cleanup steps rather than deleting by hand, and preferPOST /v2/tasks/{taskId}/retry(which resumes from the failed step) over re-running the whole template. - Works in the editor, fails for real. Dry run skips actions that lack
supportsDryRunand short-circuits handlers guarding onctx.isDryRun, so publish and webhook steps are simply never exercised. fetch:templateemits raw${{ }}or mangles a workflow file. The variable was not passed throughinput.values, or a file containing its own template syntax neededcopyWithoutTemplating.- A Nunjucks snippet from the web does not work. The engine is Nunjitsu, a subset. Check its compatibility guide before concluding the template is broken.
- Users see a step or parameter they should not. Tags in
backstage:permissionsdo nothing on their own; a policy must return a conditional decision ontemplateStepReadPermission/templateParameterReadPermission. Absent that, the parameter schema is served to everyone who can read the template.
Do not
- Do not run a template whose steps include
publish:*,catalog:register, or any infra-creating action against a shared or production SCM org without an explicit stop-and-get-authorization step. Use a scratch org or a dry run. - Do not install or write an action that executes arbitrary shell input from a template parameter; that turns any template author into a backend-host RCE.
- Do not build paths with
path.join(ctx.workspacePath, ...)or accept absolute paths from input — useresolveSafeChildPath. - Do not put tokens, passwords, or keys in
parameters, inoutput.text, or in actx.loggerdump ofctx.input. - Do not use dashed ids for steps or custom actions.
- Do not use
${{ parameters.x }}inside skeleton files, or expectbackstage:featureFlagto gate a step. - Do not allow
Templateentities from repositories outside the trusted set, and do not add a third-party action package without reading its handler. - Do not hardcode an action's input keys from memory when
/create/actionspublishes its schema.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: bendaamerahmed
- Source: bendaamerahmed/backstage-idp-plugin
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.