Writing custom Backstage scaffolder actions

Custom scaffolder actions let your templates call your own systems when built-in steps fall short. This page covers when to write one, how to build and register it, and how to document it.

On this page

When teams need custom Backstage scaffolder actions

Platform teams write a custom scaffolder action when a software template must call an internal system that no built-in action covers, such as a ticketing API, a cost-center lookup or an internal certificate service. In Backstage, a scaffolder action is a named step that a template runs in order, with typed inputs and outputs that the template form and later steps can read. Actions are defined with createTemplateAction from the scaffolder node library, where you declare an id, an input schema and an output schema, then write a handler that receives a context object. Software Templates is one of the core built-in features listed in the Backstage architecture overview, alongside the Software Catalog and TechDocs, and the scaffolder that executes templates runs on the backend, not in the browser. That placement matters: your action code runs with backend credentials and network access, so it must be reviewed like any other service code.

The decision to write a custom action rather than use another mechanism comes down to where the logic should live and who must maintain it. A custom action is the right choice when the step must return structured output that later template steps consume, when the same call is needed by many templates, or when the step must be governed by the same catalog and permission model as the rest of the template. A shell step or an external workflow can be faster to prototype, but output is harder to type, failures are harder to attribute in the task log, and the logic ends up duplicated across templates. Before you start, confirm which built-in actions your Backstage version already ships, since the scaffolder action set changes between releases, and check the current action authoring guidance in the Backstage documentation for your version.

Scope the work by listing each external call the template needs, the inputs it depends on, and the values it must return to later steps. Decide who owns the action after launch, because every custom action becomes part of the platform's upgrade path when @backstage packages move together during version bumps. Ask your team to confirm three things early: that the target system has an API or client library with a stable contract, that the action can be tested without creating live resources, and that its credentials can be scoped to the minimum the step needs. These checks determine most of the effort, and they shape the naming, schema and testing decisions covered in the sections that follow.

Choosing how to extend the scaffolder

This table compares four ways to add behavior to a Backstage template: built-in actions, a custom action in a backend module, a custom action in a separate internal package, and an external workflow the template triggers. Read each row against your own upgrade cadence and review process, and start with the lowest option that meets the need.

OptionBest fitUpgrade maintenanceWho reviews it
Built-in actionsFetching skeletons, publishing to a supported Git host, and registering catalog entities with existing actionsLow, since changes arrive with Backstage packages you already upgradePlatform team reviews template YAML only
Custom action in a backend moduleLogic that must run inside Backstage, such as calling an internal API with credentials the scaffolder already holdsMedium, because the module must compile against each @backstage release and is bumped with the appPlatform engineers review code and tests in the Backstage repository
Custom action in a separate internal packageShared actions used by several Backstage instances or teams that need their own release cycleMedium to high, since the package needs its own versioning and a check against each Backstage upgradeOwning team plus platform security review before the package is published internally
External workflow triggered by the templateLong-running or heavy work, tools written in another language, or steps that need a separate approval systemLow inside Backstage, but the workflow platform carries its own upgrades and credentialsOwners of the external system review the workflow, and the platform team reviews the trigger and token scope

How to build and register a custom action

This sequence takes a custom scaffolder action from an empty backend module to a named, schema-checked action that appears in the scaffolder's action list. Each step ends with a check so that a faulty action is caught before any template depends on it.

  1. Scaffold the backend module

    A platform engineer creates a backend module package in the Backstage repository, following the layout the Backstage CLI generates for new plugin modules. The package declares a dependency on the scaffolder node library and on the scaffolder backend extension point, and it is added to the backend app's dependencies. Done means the package builds and the backend starts with the module loaded but no actions registered yet.

  2. Define the input schema with zod

    Write a zod schema for every input the action accepts, marking required fields and giving each field a description that appears in the template editor. Keep inputs narrow, using enums and string formats where the values are known, because the schema is the first line of validation. Check the schema with a few sample payloads in a scratch test: valid inputs should parse, and a missing required field, a wrong type, or an unexpected enum value should be rejected.

  3. Implement the handler with createTemplateAction

    Call createTemplateAction with a unique action ID, a description, the zod schema as its input, and a handler that receives the parsed input and the action context. Use the context to write logs and to set outputs that later steps and the success page can reference. Keep the handler focused on one side effect so that failures are easy to locate in the task log. Done means the handler compiles and a direct unit call with valid input produces the expected output.

  4. Choose and verify the action ID

    Before registering, confirm the ID does not collide with an existing action. Built-in actions and actions from installed modules share one namespace, so a generic ID such as create-repo can clash with another team's action. Use a team or domain prefix, such as an organization name followed by a verb and object, and search the installed actions and your module list for the same string. Record the chosen ID in your module documentation so that template authors can find it.

  5. Register through the scaffolder extension point

    Create a backend module that depends on the scaffolder extension point and calls its addActions method with your action. Register the module in the backend index so it loads at startup. The backend will fail to start if the same ID is registered twice, so a successful start with no duplicate-registration error is the first sign that registration worked. Confirm the module name is unique as well, since two modules with the same name are a common source of startup problems.

  6. Confirm the action appears in the action list

    Start the backend and open the scaffolder's action list, either through the API endpoint for installed actions or the templates documentation view in the Backstage frontend. Your action should appear with its ID, description, and the input fields generated from the zod schema. If it is missing, check that the module is loaded in the backend index and that the ID matches what the template will call. Done means the action is listed and its fields match the schema you wrote.

  7. Test the action in a template before release

    Add a short template that calls the action with valid inputs and run it in a non-production environment. Then run it with a deliberately invalid input to confirm the schema rejects it before the handler runs. Review the task log for the messages your handler wrote. Ask your platform team whether they require a dry-run path or a review step before the action is available to all teams, since the scaffolder's behavior for side effects depends on how the handler is written.

Naming actions and designing zod schemas to avoid collisions

Action IDs share a single namespace across every scaffolder module loaded into your Backstage backend, so the name you choose for a custom action is a shared contract, not a local detail. When two installed modules register an action with the same ID, the scaffolder cannot give both the same meaning, and the result is either a startup failure or a step that quietly runs the wrong handler. Before you register anything, list the action IDs already in use by the built-in modules and every third-party module you plan to install, and check the names against that list. Confirm the exact registration behavior for duplicate IDs in the version of @backstage/plugin-scaffolder-backend you run, because that behavior decides how much damage a clash can do.

Adopt a prefix convention that names the owner and the domain, such as acme: followed by a verb and object, for example acme:billing:provision-ledger-account. A company or team prefix keeps your actions out of the namespace used by upstream and community modules, and it tells a template author who to ask when a step fails. Keep the verb consistent across actions, and avoid names that describe a single template, since a name tied to one project will outlive that project. Put the convention in your platform standards and enforce it in code review, because an ID is hard to rename once templates depend on it.

Define the inputSchema and outputSchema with zod so that bad parameters fail at the start of the step, before any repository, webhook or cloud resource is created. Mark fields as required unless there is a real default, constrain strings with patterns or enums where the target system is strict, and validate cross-field rules with refinements. Schemas also describe the action to template authors, so a clear field description saves review time. Treat every schema change as a change to a public interface: adding an optional field is usually safe, while renaming a field, tightening a constraint or removing an output breaks each template that calls the action. Version the action ID or run a deprecation period, and find every template that references it before you release the change.

Documenting actions under /create/actions

Template authors use the actions reference to decide which steps they can call and what each step expects. They do not need the TypeScript internals, but they do need the exact action ID to write in a template.yaml step, the input names and types, and which inputs are required. A good entry shows the action ID as it must appear in the action field, then lists each input with its type and whether it can be omitted. Authors will copy the worked example first, so the example should use realistic parameter references from the template's parameters block.

  • Exact action ID: the string that goes in the step's action field, copied from the registration code so typos are avoided.
  • Input table: each input name, type, required or optional status and default value.
  • Output names: the values later steps can read through steps.<id>.output, so chaining works without guesswork.
  • Worked example: a short step block showing inputs wired to template parameters.
  • Failure notes: the common error when a required input is missing or a value fails validation.

Check that every example in the reference runs against a current Backstage version before publishing it.

Security risks in custom scaffolder actions

A custom action runs inside the Backstage backend with the credentials it is given, so every input, external call and log line becomes part of your attack surface. The cards below pair each risk with a control the platform team owns, from credential scope to logging hygiene. The threat model hardening topic covers scaffolder risks in depth.

Credentials passed through inputs

Actions that accept a token as a template parameter put that secret in the template's form data, the task record and sometimes the browser history. Keep credentials in the backend configuration instead, and read them through the config API inside the handler. Check that no template parameters block asks users for a token, and that the action reads secrets only from integrations configuration.

Actions that create external resources

An action that creates a repository, project or cloud resource acts with the permissions of its token, not with the permissions of the person who ran the template. A single template run can therefore create resources that the requester could never create directly. Ask which account the action uses, scope that account to the minimum needed, and confirm who may run the template before publishing it.

Template parameters reaching shell and HTTP calls

Parameter values are user input. If an action passes them into a shell command or builds a URL by string concatenation, a crafted value can change the command or redirect the request to an internal host. Pass arguments as arrays to process APIs, validate values with the zod schema using tight patterns, and encode path segments before calling an HTTP endpoint.

Logging that leaks secrets

Task logs are visible to everyone who can view the task list, and an action that calls ctx.logger with a full request or response can write tokens or personal data into them. Log identifiers such as repository names and task IDs, mask any value that came from a credential, and review logging in each action's unit tests. Confirm that your task log retention matches your data policy.

Unreviewed action code in production

A custom action is backend code that runs with the access of the Backstage process, so a change to it can alter what every template does. Require code review, dependency checks and a named owner for each action module before it is registered in the backend. Keep the action names stable so reviewers can see when an existing action changes behavior.

Limits to confirm before you ship an action

A custom scaffolder action runs inside a Backstage release that changes every month, so its behavior depends on the version you run. The cards below list the checks that connect the action's code to the platform you actually operate, from the Node.js line to retry behavior and dry-run coverage.

Backstage version and release line

Confirm which Backstage release your instance runs and whether you follow the main line or the weekly next line. The main line ships monthly, and the next line ships weekly on Tuesdays, so an API you rely on may change within a few weeks. Pin the version your action was tested against, and record it next to the action's code. Check the release timing in the versioning policy before you schedule a rollout.

Node.js support line

Backstage supports two adjacent even-numbered Node.js releases, and the pair depends on the Backstage version. Your action's dependencies, including the zod version used for input schemas, must install and run on that Node.js line. Confirm the pair for your release before you add a dependency or change your base image, because a mismatch fails at startup rather than at template run time.

Stability of the scaffolder API

Confirm that every import you use from the scaffolder packages, including createTemplateAction and the action context fields, is documented as stable in your release. Some scaffolder interfaces change between versions, and a type error at build time is easier to catch than a silent change in behavior. Keep the action's imports in one module so an upgrade touches one file, and read the upgrade notes for each release you adopt.

Behavior on retry

Decide what the action does when a run fails halfway and someone retries or restarts it. An action that creates a repository, a webhook or a cloud resource can produce duplicates if the first call succeeded but the run reported a failure. Ask whether each side effect can check for existing state before it writes, and document the outcome for operators. Confirm how your release handles cancellation and restart before you rely on either.

Dry-run and unit testing

Confirm that every write in the action is guarded by a dry-run check, using the context flag for dry runs, so reviewers can exercise the template without creating anything. Test the handler as a plain function with mocked clients, and keep the schema tests separate from the handler tests. Ask who reviews the dry-run output and whether that review happens before the template reaches teams.

Credentials and scope of the action

List which tokens, app installations or service accounts the action uses and the narrowest scope each one needs. Confirm that the action reads secrets through the configured integrations and never logs them in the task output. Ask your security team to review the scope against the permissions your Git platform and cloud accounts grant, since a template run acts with whatever rights its credentials hold.

Questions teams ask before writing custom actions

How much does it cost to write a custom Backstage scaffolder action?

Cost is driven by the work the action has to do, not by the framework. Most of the spend is engineering time for the handler, its zod input schema, error handling, dry-run behavior, unit tests and documentation under /create/actions. Backstage itself is open source under the Apache License 2.0 (Apache-2.0), so there is no license fee for the framework. Ask any partner for a fixed-scope quote that lists each action, the external systems it calls, the credentials it needs and the test and review steps. Recurring cost comes later, when the action must be kept compatible with each Backstage release.

How long does a custom action take to build?

Duration depends on how many external systems the action touches and how much of its behavior must be verified before a template uses it. A single action that creates one resource through a documented API is a small task. An action that calls several systems, must be idempotent, and needs retry-safe checkpoints takes longer. Ask for a timeline tied to a written scope, and confirm when the team expects to review the action in a non-production Backstage instance before any template uses it.

What does the team need to provide for an action to be built?

The work needs access and decisions more than code. Your team should provide the target system's API documentation, a non-production account or sandbox, the service credentials and their scopes, the naming convention for actions, and a named owner who will review the code. You also need a decision on who may run any template that calls the action, since that drives permission design. A reviewer who knows the target system is important, because the action will be trusted with its write operations.

Can an in-house engineer write a custom scaffolder action?

Yes, if the engineer knows TypeScript, the Backstage backend plugin model and the target system's API. The action is a function registered through createTemplateAction, with a zod schema for inputs and a handler that uses the scaffolder context. The harder skills are idempotency, secret handling and writing tests that do not create real resources. Teams new to Backstage often underestimate the backend module registration and the review step. A partner can build the first action alongside your engineers so the pattern is documented and repeatable.

How do Backstage upgrades affect custom scaffolder actions?

Actions depend on Backstage packages, so they must move with the rest of the application. Backstage recommends moving all @backstage packages together with yarn backstage-cli versions:bump, and the Backstage Upgrade Helper shows template changes between versions. Your action code can break when the scaffolder API or its types change, and it can also break when the supported Node.js versions change. Ask whether the action has tests that run in CI on each upgrade, and who owns fixing it when a new release lands.

What are the main risks of writing a custom action?

The main risks are unintended writes, leaked credentials and duplicate resources. An action that creates a repository or cloud object can run again after a failure, so it needs checkpoints and a clear behavior on retry. Inputs from the form must be validated with the zod schema, and secrets must come from the configuration, not from template parameters. Check how the action reports errors, and make sure it respects dry-run mode so template reviews do not create real resources.

When should a team avoid writing a custom action at all?

Avoid a custom action when a built-in action or an existing community action already does the job, or when the step can be handled by the target platform itself, such as a repository template or a pipeline. Also avoid it when the logic changes often and would need constant maintenance. A custom action is worth building when a step is repeated across many templates, when it writes to a system only you operate, or when it enforces a control that auditors will test. Check the built-in action list before starting.

How DeployOpen scopes and delivers custom Backstage actions

DeployOpen approaches custom scaffolder actions as part of a wider Backstage build, starting from the template that needs the action and the systems the action must reach. We scope, build, test and document each action inside your own infrastructure, then hand over code, tests and runbooks your team can maintain after the engagement ends.

How we scope

  • Inventory the templates and steps that built-in actions cannot complete today.
  • Agree the input and output schema for each action in writing before coding starts.
  • Set the naming convention, owning team and access rules for each new action.

How we build

  • Implement each action with createTemplateAction and zod schemas for inputs and outputs.
  • Register actions through a backend module and check for name collisions before release.
  • Write unit tests for handlers, including dry-run behavior for every side-effecting step.

How we hand over

  • Deliver source code, tests and examples documented under the actions reference path.
  • Provide a short runbook covering registration, configuration, credentials and rollback steps.
  • Walk your platform team through the code so they can extend actions without us.

How we support

  • Engagements are fixed-scope, with scope, deliverables and timelines agreed in writing.
  • Ongoing support, Backstage upgrades and continuous testing are available as a retainer.
  • Retainers can cover action compatibility checks when Backstage versions change.

Sources and further reading

Talk to our team.

Tell us what you're working on, whether it's a deployment, an audit, a security test or a cyber range. You'll speak with an engineer who can help you scope it.

  • 30-minute call: free, with no obligation.
  • NDA on request: we can sign before you share details.
  • Clear next steps: a scope and plan after the call.