Custom Actions

Integrate workflow, automation and other other custom tasks into the visual editor.

Custom actions create the ability to perform site and content tasks in a single location.

Stackbit 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 the 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 Action Trigger
Global Action Trigger

Global actions are configured as a property in the main Stackbit configuration object.

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
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.

Bulk Action Modal
Bulk Action Modal

Like global actions, bulk actions are configured as a property in the main Stackbit configuration object, specified by the type property.

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
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.

Model Action Sidebar
Model Action Sidebar

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 Action Toolbar
Model Action Toolbar

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.)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
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.

Field Action Sidebar
Field Action Sidebar

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 Action Toolbar
Field Action Toolbar

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.

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
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.

Accepting Input

Actions can accept input from editors by supplying the inputFields property with field definitions. These field definitions are identical to Stackbit 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

Using 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:

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
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. Stackbit 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.

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
const actions = [
  {
    run: async (options) => {
      // Do something with the translation API ...
      return { state: 'running' }
    },
  },
]

This overrides the default Stackbit 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.

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
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.

Generating 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.

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
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.)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
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')
          },
        },
      ],
    },
  ],
}