Skip to main content

Writing Custom Actions

If you want to extend the functionality of the Scaffolder, you can do so by writing custom actions which can be used alongside our built-in actions.

Note: When adding custom actions, the actions array will replace the built-in actions too. Meaning, you will no longer be able to use them. If you want to continue using the builtin actions, include them in the actions array when registering your custom actions, as seen below.

Writing your Custom Action

Your custom action can live where you choose, but simplest is to include it alongside your backend package in packages/backend.

Let's create a simple action that adds a new file and some contents that are passed as input to the function.

In packages/backend/src/plugins/scaffolder/actions/custom.ts we can create a new action.

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

export const createNewFileAction = () => {
return createTemplateAction({
id: 'acme:file:create',
schema: {
input: z.object({
contents: z.string().describe('The contents of the file'),
filename: z
.string()
.describe('The filename of the file that will be created'),
}),
},

async handler(ctx) {
await fs.outputFile(
`${ctx.workspacePath}/${ctx.input.filename}`,
ctx.input.contents,
);
},
});
};

So let's break this down. The createNewFileAction is a function that returns a createTemplateAction, and it's a good place to pass in dependencies which close over the TemplateAction. Take a look at our built-in actions for reference.

The createTemplateAction takes an object which specifies the following:

  • id - a unique ID for your custom action. We encourage you to namespace these in some way so that they won't collide with future built-in actions that we may ship with the scaffolder-backend plugin.
  • schema.input - A zod or JSON schema object for input values to your function
  • schema.output - A zod or JSON schema object for values which are output from the function using ctx.output
  • handler - the actual code which is run as part of the action, with a context

You can also choose to define your custom action using JSON schema instead of zod:

With JSON Schema
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
import { writeFile } from 'fs';

export const createNewFileAction = () => {
return createTemplateAction<{ contents: string; filename: string }>({
id: 'acme:file:create',
schema: {
input: {
required: ['contents', 'filename'],
type: 'object',
properties: {
contents: {
type: 'string',
title: 'Contents',
description: 'The contents of the file',
},
filename: {
type: 'string',
title: 'Filename',
description: 'The filename of the file that will be created',
},
},
},
},
async handler(ctx) {
const { signal } = ctx;
await writeFile(
`${ctx.workspacePath}/${ctx.input.filename}`,
ctx.input.contents,
{ signal },
_ => {},
);
},
});
};

Naming Conventions

Try to keep names consistent for both your own custom actions, and any actions contributed to open source. We've found that a separation of : and using a verb as the last part of the name works well. We follow provider:entity:verb or as close to this as possible for our built in actions. For example, github:actions:create or github:repo:create.

Also feel free to use your company name to namespace them if you prefer too, for example acme:file:create like above.

Prefer to use camelCase over snake-case for these actions if possible, which leads to better reading and writing of template entity definitions.

We're aware that there are some exceptions to this, but try to follow as close as possible. We'll be working on migrating these in the repository over time too.

The context object

When the action handler is called, we provide you a context as the only argument. It looks like the following:

  • ctx.baseUrl - a string where the template is located
  • ctx.logger - a Winston logger for additional logging inside your action
  • ctx.logStream - a stream version of the logger if needed
  • ctx.workspacePath - a string of the working directory of the template run
  • ctx.input - an object which should match the zod or JSON schema provided in the schema.input part of the action definition
  • ctx.output - a function which you can call to set outputs that match the JSON schema or zod in schema.output for ex. ctx.output('downloadUrl', myDownloadUrl)
  • createTemporaryDirectory a function to call to give you a temporary directory somewhere on the runner so you can store some files there rather than polluting the workspacePath
  • ctx.metadata - an object containing a name field, indicating the template name. More metadata fields may be added later.

Registering Custom Actions

Once you have your Custom Action ready for usage with the scaffolder, you'll need to pass this into the scaffolder-backend createRouter function. You should have something similar to the below in packages/backend/src/plugins/scaffolder.ts

return await createRouter({
containerRunner,
catalogClient,
logger: env.logger,
config: env.config,
database: env.database,
reader: env.reader,
});

There's another property you can pass here, which is an array of actions which will set the available actions that the scaffolder has access to.

import { createBuiltinActions } from '@backstage/plugin-scaffolder-backend';
import { ScmIntegrations } from '@backstage/integration';
import { createNewFileAction } from './scaffolder/actions/custom';

export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
const catalogClient = new CatalogClient({ discoveryApi: env.discovery });
const integrations = ScmIntegrations.fromConfig(env.config);

const builtInActions = createBuiltinActions({
integrations,
catalogClient,
config: env.config,
reader: env.reader,
});

const actions = [...builtInActions, createNewFileAction()];

return createRouter({
actions,
catalogClient: catalogClient,
logger: env.logger,
config: env.config,
database: env.database,
reader: env.reader,
});
}

List of custom action packages

Here is a list of Open Source custom actions that you can add to your Backstage scaffolder backend:

NamePackageOwner
Yeomanplugin-scaffolder-backend-module-yeomanBackstage
Cookiecutterplugin-scaffolder-backend-module-cookiecutterBackstage
Railsplugin-scaffolder-backend-module-railsBackstage
HTTP requestsscaffolder-backend-module-http-requestRoadie
Utility actionsscaffolder-backend-module-utilsRoadie
AWS cli actionsscaffolder-backend-module-awsRoadie
Scaffolder .NET Actionsplugin-scaffolder-dotnet-backendAlef Carlos
Scaffolder Git Actionsplugin-scaffolder-git-actionsDrew Hill
Azure Pipeline Actionsscaffolder-backend-module-azure-pipelinesParfümerie Douglas
Azure Repository Actionsscaffolder-backend-module-azure-repositoriesParfümerie Douglas
Snyk Import Projectplugin-scaffolder-backend-module-snykMatthew Thomas
JSON Merge Actionsplugin-scaffolder-json-merge-actionsDrew Hill
NPM Actionsplugin-scaffolder-npm-actionsDrew Hill
Slack Actionsplugin-scaffolder-backend-module-slackDrew Hill
Microsoft Teams Actionsplugin-scaffolder-backend-module-ms-teamsGaurav Pandey

Have fun! 🚀