Project: Backstage (App Definition and Development / Application Definition & Image Build)
File: writing-custom-actions.md

id: writing-custom-actions title: Writing Custom Actions description: How to write your own 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 . :::note 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. ::: 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. ```ts title="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 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`: ```ts title="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 }, _ => {}, ); }, }); }; ``` 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

We'll be working on migrating these in the repository over time too. 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. 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` ```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. ```ts 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, }); } ``` To register your new custom action in the New Backend System you will need to create a backend module. Here is a very simplified example of how to do that: ```ts title="packages/backend/src/index.ts" / highlight-add-start / import { scaffolderActionsExtensionPoint } from '@backstage/plugin-scaffolder-node/alpha'; import { createBackendModule } from '@backstage/backend-plugin-api'; / highlight-add-end / / highlight-add-start / const scaffolderModuleCustomExtensions = createBackendModule({ pluginId: 'scaffolder', // name of the plugin that the module is targeting moduleId: 'custom-extensions', register(env) { env.registerInit({ deps: { scaffolder: scaffolderActionsExtensionPoint, // ... and other dependencies as needed }, async init({ scaffolder / ..., other dependencies / }) { // Here you have the opportunity to interact with the extension // point before the plugin itself gets instantiated scaffolder.addActions(new createNewFileAction()); // just an example }, }); }, }); / highlight-add-end / const backend = createBackend(); backend.add(import('@backstage/plugin-scaffolder-backend/alpha')); / highlight-add-next-line / backend.add(scaffolderModuleCustomExtensions()); ``` Here is a list of Open Source custom actions that you can add to your Backstage scaffolder backend: | Name | Package | Owner | | | -- | | | Yeoman | | | | Cookiecutter | | | | Rails | | | | HTTP requests | | | | Utility actions | | | | AWS cli actions | | | | Scaffolder .NET Actions | | | | Scaffolder Git Actions | | | | Azure Pipeline Actions | | | | Azure Repository Actions | | | | Snyk Import Project | | | | JSON Merge Actions | | | | NPM Actions | | | | Slack Actions | | | | Microsoft Teams Actions | | | Have fun!