Actions Registry (alpha)
Overview
The Actions Registry Service is a core service designed to provide a distributed registry for actions that can be executed within Backstage backend plugins. This service allows plugins to register reusable actions with well-defined schemas and execution logic, promoting consistency and reusability across the Backstage ecosystem.
Action Structure
Each action registered with the service must conform to the ActionsRegistryActionOptions type, which includes:
Required Properties
name: A unique identifier for the action (string)title: A human-readable title for the action (string)description: A detailed description of what the action does (string)schema: Object containing schema definitionsinput: Function that returns a Zod schema for validating inputoutput: Function that returns a Zod schema for validating outputsecrets: (optional) Function that returns a Zod schema for validating secrets. See Secrets below.
action: The async function that executes the action logic
Optional Properties
visibilityPermission: ABasicPermissionthat controls visibility and access to the action through the permissions framework. See Permissions below.attributes: Object containing behavioral flags:destructive: Boolean indicating if the action modifies or deletes dataidempotent: Boolean indicating if running the action multiple times produces the same resultreadOnly: Boolean indicating if the action only reads data without modifications
Action Context
When an action is executed, it receives a context object (ActionsRegistryActionContext) containing:
input: The validated input data matching the defined input schemasecrets: The validated secrets data matching the defined secrets schema, orundefinedif no secrets schema is declaredlogger: A LoggerService instance for logging within the actioncredentials: BackstageCredentials for authentication and authorization
Using the Service
Registering an Action
Here's an example of how to register an action with the Actions Registry Service:
import { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha';
export function registerMyActions(actionsRegistry: ActionsRegistryService) {
// Register a simple read-only action
actionsRegistry.register({
name: 'fetch-user-info',
title: 'Fetch User Information',
description: 'Retrieves user information from the catalog',
schema: {
input: z =>
z.object({
userRef: z.string(),
includeGroups: z.boolean().optional(),
}),
output: z =>
z.object({
user: z.object({
name: z.string(),
email: z.string(),
groups: z.array(z.string()).optional(),
}),
}),
},
attributes: {
readOnly: true,
idempotent: true,
},
action: async ({ input, logger, credentials }) => {
logger.info(`Fetching user info for ${input.userRef}`);
// Perform the action logic here
const user = await fetchUserFromCatalog(input.userRef, credentials);
return {
output: {
user: {
name: user.name,
email: user.email,
groups: input.includeGroups ? user.groups : undefined,
},
},
};
},
});
// Register a destructive action
actionsRegistry.register({
name: 'delete-entity',
title: 'Delete Entity',
description: 'Removes an entity from the catalog',
schema: {
input: z =>
z.object({
entityRef: z.string(),
force: z.boolean().optional(),
}),
output: z =>
z.object({
deletedEntities: z.array(z.string()),
}),
},
attributes: {
destructive: true,
idempotent: false,
},
action: async ({ input, logger, credentials }) => {
logger.warn(`Deleting entity ${input.entityRef}`);
// Perform the deletion logic here
const { deletedEntities } = await deleteEntityFromCatalog(
input.entityRef,
input.force,
credentials,
);
return {
output: deletedEntities,
};
},
});
}
Accessing the Service in a Plugin
To use the Actions Registry Service in your plugin, access it through dependency injection:
import {
createBackendPlugin,
coreServices,
} from '@backstage/backend-plugin-api';
import { actionsRegistryServiceRef } from '@backstage/backend-plugin-api/alpha';
export const myPlugin = createBackendPlugin({
pluginId: 'my-plugin',
register(env) {
env.registerInit({
deps: {
actionsRegistry: actionsRegistryServiceRef,
logger: coreServices.logger,
},
async init({ actionsRegistry, logger }) {
logger.info('Registering actions...');
registerMyActions(actionsRegistry);
logger.info('Actions registered successfully');
},
});
},
});
Permissions
Actions can optionally declare a visibilityPermission to control visibility and access through the Backstage permissions framework. The visibilityPermission must be a BasicPermission (not a resource permission). When set, the action is only visible in listings and accessible by callers who are authorized.
When accessed via the Actions Service or the /.backstage/actions/v1/... HTTP endpoints, actions that are denied by the permission policy are filtered from list results and return a 404 Not Found on invocation, as if they don't exist.
Permissions declared on actions are automatically registered with the PermissionsRegistryService so they appear in the permission policy system.
Adding a Permission to an Action
import { createPermission } from '@backstage/plugin-permission-common';
// Define a permission for your action
const myDeletePermission = createPermission({
name: 'my-plugin.actions.deleteEntity',
attributes: { action: 'delete' },
});
actionsRegistry.register({
name: 'delete-entity',
title: 'Delete Entity',
description: 'Removes an entity from the catalog',
visibilityPermission: myDeletePermission,
schema: {
input: z => z.object({ entityRef: z.string() }),
output: z => z.object({ deleted: z.boolean() }),
},
action: async ({ input }) => {
// action logic
return { output: { deleted: true } };
},
});
Actions without a visibilityPermission field remain visible and accessible by all callers, preserving backwards compatibility.
Secrets
Actions can declare a secrets schema to request external credentials from the end user, such as API tokens, personal access tokens, or other sensitive values that are not part of Backstage's own authentication system. Secrets are kept separate from the input schema so they never appear in tool definitions or LLM context when actions are exposed as MCP tools.
Declaring a Secrets Schema
Add a secrets function to the schema object alongside input and output. It works the same way as the input schema, receiving the Zod instance and returning a Zod object schema:
actionsRegistry.register({
name: 'create-issue',
title: 'Create GitHub Issue',
description: 'Creates an issue in a GitHub repository',
schema: {
input: z =>
z.object({
repo: z.string(),
title: z.string(),
body: z.string().optional(),
}),
output: z =>
z.object({
issueUrl: z.string(),
}),
secrets: z =>
z.object({
githubToken: z
.string()
.describe('GitHub Personal Access Token with repo scope'),
}),
},
attributes: {
destructive: false,
},
action: async ({ input, secrets, credentials }) => {
const octokit = new Octokit({ auth: secrets.githubToken });
const { data } = await octokit.issues.create({
owner: input.repo.split('/')[0],
repo: input.repo.split('/')[1],
title: input.title,
body: input.body,
});
return { output: { issueUrl: data.html_url } };
},
});
The secrets field in the action context is fully typed based on the declared schema. Actions without a secrets schema receive undefined for the secrets field.
How Secrets Flow Through the System
Secrets are validated against the Zod schema the same way input is validated. If secrets are required but not provided, or if they fail validation, the action returns an InputError. If secrets are provided to an action that does not declare a secrets schema, the request is also rejected.
The secrets schema is included in the action metadata returned by the list endpoint, so callers can discover which secrets an action requires before invoking it.
Best Practices
Naming Conventions
- Use kebab-case: Action names should be in kebab-case (e.g.,
fetch-user-info,create-repository) - Be Descriptive: Choose names that clearly describe what the action does
- Avoid Redundancy: Don't include plugin names in action names since the plugin context is separate
- Use Verbs: Start action names with verbs that describe the operation (e.g.,
fetch,create,delete,update)
Action Attributes Reference
| Attribute | Type | Default | Description |
|---|---|---|---|
destructive | boolean | true | Indicates the action modifies or deletes data. Use with caution. |
idempotent | boolean | false | Indicates the action can be run multiple times with the same result |
readOnly | boolean | false | Indicates the action only reads data without making modifications |
These attributes help consumers of actions understand their behavior and implement appropriate safeguards, retries, or optimizations based on the action's characteristics.