Creating Backstage Scaffolder Actions

Extending the possibilities with software templates

Heikki Hellgren
5 min readFeb 5, 2024
Backstage logo © Spotify

Backstage is an open platform for building developer portals for companies with software assets. Developing your portal on top of Backstage is relatively straightforward; just create an app and configure it to your liking.

Backstage includes a powerful functionality to automatize all kinds of processes called Software Templates. These templates are YAML files like other Backstage entities with special parameters , steps and output objects that are used to take user input, act based on the input, and finally output data for the user.

A very simple example of a template could be:

apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: create-repository
title: Create new repository
spec:
owner: backstage-developers
type: golden
parameters:
- title: New repository information
required:
- name
- description
properties:
title:
title: Name
type: string
description: Repository name
ui:autofocus: true
description:
title: Description
type: string
description: Repository description
steps:
- id: createRepository
name: Create repository
action: github:repo:create
input:
repoUrl: https://github.com/backstage/${{ parameters.name }}
description: ${{ parameters.description }}
output:
links:
- url: ${{ steps.createRepository.output.remoteUrl }}
title: 'Repository remote URL'
text:
- title: New repository created
content: Wohoo, you can start working at ${{ steps.createRepository.output.remoteUrl }}

The templates use React JSON Schema Form to render the input form and you can use the options for RJSF when defining the template parameters.

Actions, actions, actions

The template above uses only one action github:repo:create . The Backstage comes with a lot of different built-in actions and you can include more from the different plugins. Check out the repository for all scaffolder-backend-module-* plugins to find more actions to include in your instance.

Additionally, you can, or at some point, have to create your own actions. This comes into play when there are no actions to do some specific thing you like to accomplish like integrating into some internal systems or handling data inside a template.

To start creating new actions, you have to create a new plugin with the command yarn backstage-cli new . Select the type scaffolder-module and give it a good, describing name. Usually, this is the name of an external system for example jira or jenkins , depending on what your actions are targeting.

After the command has run, you have a new folder in your plugins directory with one example action in place.

Inputs and outputs

Each action is created by using the createTemplateAction function which takes parameters about the id of the action, its description, schema, and the actual action handler.

Here is an example of a simple action that demonstrates what possibilities you have in the action:

import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
import { z } from 'zod';
import { writeFile } from 'fs-extra';

export const createEncodeAction = () => {
return createTemplateAction<{
data: string;
}>({
id: 'util:encode',
description: 'Encodes string and saves it to urlEncoded.txt',
schema: {
input: z.object({
data: z.string().describe('String data to encode'),
}),
output: z.object({
encoded: z.string(),
}),
},
supportsDryRun: true,
async handler(ctx) {
// Dry run is enabled when the templates are run in the Template Editor at `/create/edit`
if (ctx.isDryRun) {
ctx.output('encoded', 'foobar');
return;
}
ctx.logger.info(
'The action logs will be visible to the user when the action is run',
);

// You can also access the current user information in action
if (ctx.user?.entity?.spec.profile?.email === 'me@example.com') {
ctx.logger.warn('This user should not be here!');
}

// Template info is also available. You can use this to prevent specific actions to only specific templates
ctx.logger.info(`We are now running ${ctx.templateInfo?.entityRef}`);

const output = encodeURIComponent(ctx.input.data);

// The workspace path is shared between actions in one template run
await writeFile(`${ctx.workspacePath}/urlEncoded.txt`, output);

ctx.output('encoded', output);
},
});
};

Let’s start from the beginning. The action has an id that is util:encode . I highly recommend scoping the action names with : into specific functionalities to make them easier to find in the Installed Actions section (by default /create/actions) of your instance. It’s also good to give your action a proper description so that people know what it does.

Next is the schema. This describes what kind of inputs and outputs your action takes and produces. I suggest using zod to define this part as it’s much easier and helps to avoid unnecessary writing.

For the next part, we jump to supportsDryRun and it should be always enabled for your actions. This allows users to run the action from Template Editor in dry-run mode and see the output as an example. It helps a lot when developing new templates.

Last but not least, you have the actual handler of the action. This is an asynchronous method that does the work. It has one parameter, ctx , which contains all data to be used in your action:

  • input — contains input from the template as described in the schema
  • output() — a function that can be called to return data to the template
  • logger — Backstage logger service that can be used to log information during an action run. Beware that all the logs are available for the user in the template run
  • isDryRun — Boolean to determine if the template is run in dry-run mode or not
  • user — Currently logged-in user as a Backstage entity
  • templateInfo — Template information where the action was launched from
  • workspacePath — Template runs workspace path, shared with other actions, and removed after the template has run
  • secrets — Task secrets that can be used in the action such as backstage token for calling instance APIs
  • createTemporaryDirectory() — function to create a temporary directory that lasts until the end of the action
  • each — Value if the action is run with each parameter. This contains input for a single invocation

Integrating your new action

To make your action available for the templates, you must add it to the scaffolder.ts actions list. See an example here.

As we speak the Backstage is working with a new backend system where the functionality for adding actions changes a bit. To support the new backend system, you should add a new module.ts file into your new plugin (if not already there by the time being), and add your action there as follows:

export const scaffolderMyModule = createBackendModule({
pluginId: 'scaffolder',
moduleId: 'my-module',
register(reg) {
reg.registerInit({
deps: {
scaffolder: scaffolderActionsExtensionPoint,
},
async init({ scaffolder, config }) {
scaffolder.addActions([createEncodeAction()]);
},
});
},
});

Once you have your action installed, test it out by modifying the example template above by adding a new step:

steps:
- id: encodeName
name: Encoding the name
action: util:encode
input:
data: ${{ parameters.name }}
- id: createRepository
name: Create repository
action: github:repo:create
input:
# You can use the output of previous steps in the next steps as:
repoUrl: https://github.com/backstage/${{ steps.encodeName.output.encoded }}
description: ${{ parameters.description }}

And now you are done! Just remember that if you create actions that could be beneficial for other Backstage adopters, maybe consider open-sourcing them for the greater good!

Want to Connect?

I am Heikki Hellgren, lead developer and technology enthusiast
at OP Financial Group.

You can connect with me on LinkedIn or X!

--

--

Heikki Hellgren

Father of two, husband and Lead Developer @ OP Financial Group. I write about things I like and things I don’t. More info @ https://drodil.kapsi.fi