Custom actions
Integrate workflow, automation and other custom tasks into Visual Editor. Custom actions create the ability to perform site and content tasks in a single location.
Visual Editor already handles normalizing and syncing content among any number of content sources. This makes the visual editing environment the perfect candidate for triggering content, workflow, automation, and other tasks for a site.
There are multiple points at which actions can hook into Visual Editor and content flow. See below for explanations, use cases, and examples.
# Types of actions
There are four types of actions:
Each action differs in the following ways:
- Trigger location (in the UI)
- Configuration options
- Callback parameters
See below for use cases and further instruction on working with each of these types.
# Global actions
Global actions are performed on the site as a whole. For example:
- Trigger a deploy preview for the current version of a site.
- Send a custom workflow event to reviewers.
- Run a performance test on the entire site.
- Check for broken links throughout the site.
These actions are triggered from the top bar, next to the site name.
Global actions are configured as a property in the main configuration object.
//stackbit.config.ts
export default defineStackbitConfig({
stackbitVersion: "0.6.0",
contentSources: [
/* ... */
],
actions: [
{
type: "global",
name: "name_of_action",
run: async options => {
// Perform the action ...
}
// Other options ...
}
]
});
See the configuration reference for more information.
# Bulk document actions
Bulk document actions are performed on a selected set of documents. For example:
- Send a set of pages to a translation service.
Editors can choose the set of documents on which to trigger the action.
Like global actions, bulk actions are configured as a property in the main configuration object, specified by the type
property.
// stackbit.config.ts
export default defineStackbitConfig({
stackbitVersion: "0.6.0",
contentSources: [
/* ... */
],
actions: [
{
type: "bulk",
name: "name_of_action",
run: async options => {
// Perform the action ...
}
// Other options ...
}
]
});
See the configuration reference for more information.
# Model actions
Model actions are performed on an individual document. For example:
- Cloning an object based on input values
- Sending a document to a translation service
- Taking a snapshot of a document in its current state
These actions can be triggered where the document context is presented. When defined, it will always appear near the title in page and content editing modes.
If using inline editing and if a proper data-sb-object-id
annotation has been provided, the triggers will also be available in the toolbar when highlighting the document.
Model actions are configured directly on the model definition. (When using a headless CMS, the model definition is an extension of the schema defined in the source.)
import { PageModel } from "@stackbit/types";
export const Post: PageModel = {
name: "Post",
type: "page",
fields: [],
// Other properties ...
actions: [
{
name: "generate-title",
label: "Generate Title",
run: async options => {
// Perform the action ...
}
}
]
};
See the reference for more information.
# Field actions
Field actions are performed against a field on a document. For example:
- Generate AI content for a specific field (optionally based on some input).
- Suggest fixing spelling and grammar.
- Fill certain fields with custom data from an external API.
- Translate a field using an external API.
These actions can be triggered wherever the field input is displayed.
If using inline editing and proper data-sb-object-id
and data-sb-field-path
annotations have been provided, the triggers will also be available in the toolbar when highlighting the field.
Field actions are configured as a property on a field within a model definition.
When using a headless CMS, the model definition is an extension of the schema defined in the source. Adding an action on a field that has been defined in an external schema only requires adding the name
to identify the field.
import { PageModel } from "@stackbit/types";
export const Post: PageModel = {
name: "Post",
type: "page",
fields: [
{
name: "title",
actions: [
{
name: "sanitize-title",
label: "Sanitize Title",
run: async options => {
// Perform the action ...
}
}
]
}
]
// Other properties ...
};
See the reference for more information.
# Accept input
Actions can accept input from editors by supplying the inputFields
property with field definitions. These field definitions are identical to Visual Editor schema field definitions.
# Supported field types
The following field types are supported:
boolean
color
date
datetime
enum
html
markdown
number
reference
slug
string
text
url
# Use input data
The input data is passed to the run
function in an inputData
object, where the key is the name of the field and the value is the user value. Here's a simple example:
// stackbit.config.ts
export default defineStackbitConfig({
actions: [
{
type: "bulk",
name: "name_of_action",
inputFields: [{ name: "prompt", type: "string", required: true }],
run: async options => {
const { prompt } = options.inputData;
// Do something with `prompt` ...
}
}
]
});
# Handling state
The action trigger can be given a state to provide feedback to the user.
The state can be set in the return object from the run
function. Visual Editor will also check for updates to the state using the state
property.
# Supported state
The following states are supported:
enabled
disabled
hidden
running
# State example
The state function is most useful in long running actions when the state of the action may depend on external factors or maybe the field values of the document itself.
Let's assume the run
function calls a translation API that submits a whole document and requires humans to translate the content.
The whole translation process may take several days. We don't expect the run
function to run for several days. Instead, after calling the translation API, the run function will return immediately and return the proper state.
const actions = [
{
run: async options => {
// Do something with the translation API ...
return { state: "running" };
}
}
];
This overrides the default Visual Editor behavior, which would change the state back to enabled
.
Every time the Studio requests the document with that action, the state
function will check with the translation service if that document is still being translated or is finished, and return a matching state.
const actions = [
{
state: async options => {
// Check translation status ...
return { state: "..." };
},
run: async options => {
// ...
}
}
];
# Examples
Here are a few more complete examples to help get started with custom actions.
# Generate a title
This is a model action that uses Faker to generate a random title. This is shared for brevity. A more useful application might send a user prompt to an AI service.
import { PageModel } from "@stackbit/types";
export const Post: PageModel = {
name: "Post",
type: "page",
fields: [{ name: "title" /* ... */ }],
actions: [
{
name: "generate-title",
label: "Generate Title",
run: async options => {
const { faker } = await import("@faker-js/faker");
const document = options.currentPageDocument;
if (!document) return;
// Send feedback in the appropriate context
const logger = options.getLogger();
logger.debug(`Running generate-title action on page: ${document.id}`);
// Generate title
const newTitle = faker.lorem.words(4);
logger.debug(`Setting title to: ${newTitle}`);
// Update the document with the new random title
options.contentSourceActions.updateDocument({
document,
userContext: options.getUserContextForContentSourceType(
document.srcType
),
operations: [
{
opType: "set",
fieldPath: ["title"],
modelField: options.model.fields!.find(
field => field.name === "title"
) as FieldString,
field: { type: "string", value: newTitle }
}
]
});
logger.debug("Finished generate-title action");
}
}
]
};
# Fix formatting on field
In this example, we can force a field into a specific format. (Note that you could more strictly enforce this behavior with document hooks.)
import { PageModel } from "@stackbit/types";
export const Post: PageModel = {
name: "Post",
type: "page",
fields: [
{
type: "string",
name: "title",
required: true,
actions: [
{
name: "sanitize-title",
label: "Sanitize Title",
inputFields: [],
run: async options => {
const document = options.currentPageDocument;
if (!document) return;
// Send feedback to the appropriate context
const logger = options.getLogger();
logger.debug(
`Running sanitize-title action on page: ${document.id}`
);
// Get the current title
const currentTitleField = document.fields.title;
if (!currentTitleField || !("value" in currentTitleField)) return;
// Clean it up
const sanitizedTitle = currentTitleField.value
.replace(/\b(\w)/g, s => s.toUpperCase())
.trim();
// Store the updated title on the document
options.contentSourceActions.updateDocument({
document,
userContext: options.getUserContextForContentSourceType(
options.parentDocument.srcType
),
operations: [
{
opType: "set",
fieldPath: ["title"],
modelField: options.modelField,
field: { type: "string", value: sanitizedTitle }
}
]
});
logger.debug("Finished sanitize-title action");
}
}
]
}
]
};
# Status messages
Status messages allow you to customize a success, error, or other type of message to show users in the visual editor.
Custom actions used for status messages use the run
method.
If a custom action is run from custom control, then the result
property can be used.
The result is returned to the user when the user executes a custom action in custom control using
window.stackbit.runCustomAction
:
const result = await window.stackbit.runCustomAction({ actionName: 'test' });
console.log(result);
// result is either `result` attribute returned by custom action
// or success/error messages if no `result` provided
# Success status example
To return a success notification, the action’s run method needs to return an object with a success property containing the success message (this is also included in the return type of the run method):
{
run: async (options) => {
return {
success: 'Action complete 🎉'
}
}
}
# Error status example
To return an error status, the custom action’s run method needs to return an object with an error property containing a human readable error message.
This message can be returned as a result of running customAction
from the custom control.
The error message will be shown in a standard red notification in the visual editor:
run: async (options) => {
return {
error: 'oops, something went wrong'
}
}
The custom action’s run method can also throw an error object. In this case, the error’s message
run: async () => {
throw new Error('oops, something went wrong');
}
Or, if the run function is asynchronous, it can reject with an error object:
run: async (options) => {
await new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('oops, something went wrong 😱'))
}, 3000)
})
}
If your handler run
method throws an error, the message of the error will be returned as an error property with the prepended string Error running action
:
async run () {
throw new Error('oops')
Did you find this doc useful?
Your feedback helps us improve our docs.