Skip to main content

Add custom actions to your app


Add custom actions to your app

Custom actions are easy way to get smaller tasks done with your apps. Custom actions can trigger modals and app interactions. When a board item or group of items are selected, any custom actions which are defined for that selection can be launched directly from the context menu.

About custom actions

🚧

Important!

Custom actions are only supported for non-public apps that will be distributed privately via a shareable authorization link from your App Settings page. This means that apps built with custom actions will not be eligible for distribution via the Miro Marketplace at this time, and cannot be widely and publicly distributed. Please follow our Changelog for further developments.

Miro apps can expose functionality to board users through app icons, panels, modals, and context menus. These are the UI extension points for your app. As a developer, you can use these extension points to render your app on a Miro board, and to make its features available to board users.

One of the extension points for custom actions is via the context menu of a board item where your app's custom action for the board item selected can be shown.

Custom actions help board users perform a task quickly, without leaving the workflow they're in. For users, this approach reduces context switching and improves productivity.

For developers, Custom Actions add value by allowing users to discover relevant functionality for an app via the context menu.

Custom actions help to:

  • Semi-automate repetitive tasks
    • A custom action in the context menu can make it easier for users to streamline their workflows. For example:
      • Convert a sticky note to a Jira task
      • Trim embedded video clips
  • Expose individual capabilities in your app and reduce the effort for end users to understand its functionality
    • A translation custom action to convert text on sticky notes, frames, and shapes to another language

In summary:

  • App developers can add custom actions to their existing apps (available for private apps).
  • The context menu shows custom actions to board users as icons with descriptive tooltips.
  • The context menu is displayed on the board when users click an item or a group of items.
  • Users can click a custom action icon: this triggers a custom event, which executes the corresponding custom action

Value

For developers:

  • Add more capabilities to your apps
  • Simplify the actions users can take with your app

For board users:

  • Semi-automate tedious and repetitive tasks
  • Leverage app capabilities more easily

Prerequisites

Before you begin, make sure that:

  • You have a Miro account.
  • You're signed in to Miro.
  • Your Miro account has a Developer team.
  • Your development environment includes Node.js 14.15 or a later version.
  • Your app is a private app, and it's not publicly available on the Miro Marketplace.
    Currently, custom actions are available only for private apps.

Recommendations

  • There is a maximum of 4 custom actions per app. If you try to assign more than 4 custom actions:
    • The app throws an error.
    • Only the first 4 registered custom actions are available in the app.
    • The custom actions are alphabetically sorted based on app name and label within the context menu.
  • Custom actions can only leverage events that are currently supported from Miro Web SDK .
  • For custom actions to be available, the app that implements them must be running on the board.

Handling inputs

Events for Custom Actions return a payload that includes details about the current selection on the board. As a result, users do not need to provide an input through the UI.

The payload that a custom action event returns is the same as the selection: update event: an array with all selected board items.
The event handler can then perform follow-up actions on the selected items. For example: get property values or update properties, and then sync the updated items.

Build your app

For an example application that implements Custom Actions, see the following example in our GitHub repo:
Custom Actions (App Example)

Add custom actions

  • Register custom actions in the headless iframe of your app so that the actions are available as long as the app is running on the board.
    For Miro apps, this is usually the index.js/.ts file.
    If you register a custom action in the panel or the modal iframe, the operation throws an error.
  • First, subscribe to the event dispatched by the custom action, then register the custom action itself. Apps cannot register custom actions that haven't been subscribed to.

Example:

await miro.board.ui.on('custom:translate-content', handler);
await miro.board.experimental.action.register(
  {
    "event": "translate-content",
    "ui": {
      "label": {
        "en": "Translate content",
      },
      "icon": "chat-two",
      "description": "Translate the content of the board items included in the current selection.",   
    },
    "scope": "local",
    "predicate": {
      "type": "text"
    },
    "contexts": {
        "item": {}
    }
  }
);

Subscribe to a custom action

custom:${string} events implement custom actions in an app.
Web SDK custom actions behave like standard Web SDK events:

  • The app subscribes to a custom action event with the on property.
  • When the custom action event is no longer necessary, the app unsubscribes from it with the off property.

Custom event naming

Custom event names always start with the custom: prefix: custom:${string}

${string} must match the value assigned to the event property when the app registers the custom action with the miro.board.experimental.action.register method.

  • The event name can contain only lowercase alphabetic characters and hyphens (^[a-z]+(-[a-z]+)\*$).
  • It cannot contain spaces.
  • It cannot be longer than 30 characters.

Example:

await miro.board.ui.on('custom:translate-content', handler);

Register a custom action

The action namespace groups a set of methods that enable apps to use custom actions.
The action namespace is currently under the experimental namespace, and it exposes a single method: register():
miro.board.experimental.action.register

You use this method to register a custom action with an app.
The method takes a single argument: an object that defines the custom action.

Example:

await miro.board.experimental.action.register(
  {
    "event": "translate-content",
    "ui": {
      "label": {
        "en": "Translate content",
      },
      "icon": "chat-two",
      "description": "Translate the content of the board items included in the current selection.",   
    },
    "scope": "local",
    "predicate": {
      "type": "text"
    },
    "contexts": {
        "item": {}
    }
  }
);

Custom action properties

Define a custom action by setting the following properties:

event

The name of the custom event that the app subscribes to, to respond with a custom action.

The event name must match the value of the ${string} placeholder of the custom:${string}event name that you pass to the on property when you subscribe the app to it.

  • The event name can contain only lowercase alphabetic characters and hyphens (^[a-z]+(-[a-z]+)\*$).
  • It cannot contain spaces.
  • It cannot be longer than 30 characters.

Example:

// Event name
"event": "translate-content"

// Custom event name that the app subscribes to.
await miro.board.ui.on('custom:translate-content', handler);

scope

scope defines the scope of the custom action and how it interacts with other capabilities in the board. This property currently supports local scope only.

selection

selection determines whether the custom action is displayed for single or multiple selected items. Use multi if you want the custom action to be displayed when the selection includes 1 or more items and they all match the predicate. Use single for the custom action to be shown when there is only one selected item and it matches the predicate. Default value: multi.

ui

The object exposes content that is shown to board users in the context menu of one or more selected board items.

ui.label

label corresponds to the public name of the custom action rendered in the item context menu.

label has a property to define the locale of the content so that you can make your custom action available in multiple languages.
en is the default locale, and it is required.
Other locale properties are optional, depending on the languages that you want to support.
You can provide localized content for the following languages:

  • en: English (required)
  • fr: French
  • de: German
  • ja_JP: Japanese
  • es: Spanish
  • pt_BR: Brazilian Portuguese
ui.icon

icon sets the custom action icon that is displayed in the context menu of one or more selected board items.

When users click the icon, the custom event is triggered; if the app listens to it, it responds by running the corresponding custom action (the event handler).

You can assign icon an alphabetic string value that represents a corresponding icon.

The value can be one of the following:

ui.description

description contains a short text string that explains what the custom action does. The description is used as the tooltip for the element in the context menu and is displayed when a user hovers over an item.
It helps board users understand the purpose of the custom action. You can pass the same translation structure as the one used in the label property.
The content cannot be longer than 80 characters.

predicate

The predicate property specifies the criteria that determine which board items a custom action applies to.

The custom action only appears in the context menu when all currently selected items match the conditions defined in predicate.

predicate evaluates each selected item individually, even with multi-selections. It checks each item separately and must return true for every item in the selection.

If any selected item fails the predicate check, the custom action does not display.

In summary, predicate allows you to selectively show custom actions based on rules that apply to the current selection. The action only appears when all selected items pass the predicate criteria.

  • The conditions that you can set with predicate correspond to the supported board item properties.
  • predicate syntax leverages the MongoDB query DSL syntax.
    This approach offers a wide range of operators that allow granular control of the availability of a custom action on the board UI.
  • In addition to an item's supported properties, you can also use metadata to target your own app persisted metadata in the item.

Example:

// Example of a predicate that makes a custom action available only when
// the selected board items are shapes, texts, or sticky notes.
"predicate": {
  "$or": [
    { type: "shape" },
    { type: "text" },
    { type: "sticky_note" },
  ]
}

More complex predicate examples:

Embed item with a specific URL

Mongo playground

Item properties sample
{
    "type": "embed",
    "previewUrl": "https://i.ytimg.com/vi/0olcwCD9-GM/hqdefault.jpg",
    "mode": "inline",
    "id": "3458764513841707078",
    "parentId": "3458764513842634009",
    "origin": "center",
    "relativeTo": "parent_top_left",
    "createdAt": "2023-06-06T09:47:55.407Z",
    "createdBy": "3458764513823195488",
    "modifiedAt": "2023-06-22T14:26:37.422Z",
    "modifiedBy": "3458764513823195488",
    "url": "https://www.youtube.com/watch?v=0olcwCD9-GM",
    "x": 1951.4079009002294,
    "y": 1979.7892875703383,
    "width": 668.8084695079418,
    "height": 581.8633684719094
  }
Predicate
{
  "type": "embed",
  "url": {
    $regex: "https?:\/\/www\\.youtube\\.com\/.*",
  },
}
App cards based on app metadata

Mongo playground

Item properties sample
{
    "type": "app_card",
    "owned": true,
    "title": "This is the title of the app card",
    "description": "The custom preview fields are highlighted in different colors; the app card icon is displayed on the bottom-right.",
    "style": {
      "cardTheme": "#2d9bf0"
    },
    "tagIds": [],
    "status": "connected",
    "id": "3458764513842634146",
    "parentId": "3458764513842634009",
    "origin": "center",
    "relativeTo": "parent_top_left",
    "createdAt": "2023-06-22T14:25:04.993Z",
    "createdBy": "3458764513823195488",
    "modifiedAt": "2023-06-22T14:26:37.422Z",
    "modifiedBy": "3458764513823195488",
    "x": 3834.982106507746,
    "y": 1684.0152684238365,
    "width": 320,
    "height": 126,
    "rotation": 0,
    "metadata": {
      "status": "in_progress"
    }
  },
Predicate
{
  "type": "app_card",
  "owned": true,
  "metadata": {
    "status": "in_progress"
  } 
}
Connectors that have both start and end items connected to them

Mongo playground

Item properties sample
[
  {
    "type": "connector",
    "shape": "curved",
    "start": {
      "item": "3458764513842214045",
      "snapTo": "auto"
    },
    "end": {
      "item": "3458764513842251902",
      "snapTo": "auto"
    },
    "style": {
      "startStrokeCap": "none",
      "endStrokeCap": "rounded_stealth",
      "strokeStyle": "normal",
      "strokeWidth": 2,
      "strokeColor": "#333333",
      "textOrientation": "horizontal"
    },
    "captions": [],
    "id": "3458764513842632916",
    "parentId": "3458764513842634009",
    "origin": "center",
    "relativeTo": "parent_top_left",
    "createdAt": "2023-06-22T14:22:05.092Z",
    "createdBy": "3458764513823195488",
    "modifiedAt": "2023-06-22T14:26:37.422Z",
    "modifiedBy": "3458764513823195488"
  },
  {
    "type": "connector",
    "shape": "curved",
    "start": undefined,
    "end": undefined,
    "style": {
      "startStrokeCap": "none",
      "endStrokeCap": "rounded_stealth",
      "strokeStyle": "normal",
      "strokeWidth": 2,
      "strokeColor": "#333333",
      "textOrientation": "horizontal"
    },
    "captions": [],
    "id": "3458764513842632918",
    "parentId": "3458764513842634009",
    "origin": "center",
    "relativeTo": "parent_top_left",
    "createdAt": "2023-06-22T14:22:29.593Z",
    "createdBy": "3458764513823195488",
    "modifiedAt": "2023-06-22T15:20:23.148Z",
    "modifiedBy": "3458764513823195488"
  }
]
Predicate
{
  type: "connector",
  start: {
    $exists: true,
  },
  end: {
    $exists: true,
  },
}
Mindmap root node

Mongo playground

Item properties sample
{
  "type": "mindmap_node",
  "id": "3458764513842632921",
  "parentId": "3458764513842634009",
  "origin": "center",
  "relativeTo": "parent_top_left",
  "createdAt": "2023-06-22T14:23:01.782Z",
  "createdBy": "3458764513823195488",
  "modifiedAt": "2023-06-22T14:26:37.422Z",
  "modifiedBy": "3458764513823195488",
  "x": 909.0731198190688,
  "y": 3559.9800151623404,
  "width": 1042.857142857143,
  "height": 457.14285714285717,
  "nodeView": {
    "type": "text",
    "content": "<p>ROOT NODE</p>"
  },
  "childrenIds": [
    "3458764513842632922"
  ],
  "isRoot": true,
  "layout": "horizontal"
}
Predicate
{
  type: "mindmap_node",
  isRoot: true
}
Images that are not rotated

Mongo playground

Item properties sample
{
  "type": "image",
  "title": "",
  "id": "3458764513842250250",
  "parentId": "3458764513842634009",
  "origin": "center",
  "relativeTo": "parent_top_left",
  "createdAt": "2023-06-14T10:20:47.262Z",
  "createdBy": "3458764513823195488",
  "modifiedAt": "2023-06-22T14:26:37.422Z",
  "modifiedBy": "3458764513823195488",
  "x": 1404.0050456061917,
  "y": 1134.4583650796958,
  "width": 581.8633684719098,
  "height": 581.8633684719098,
  "rotation": 0,
  "url": "https://loremflickr.com/500/500?v=2",
  "metadata": {
    "origin": "plantuml"
  }
 }
Predicate
{
  type: "image",
  rotation: 0
}
Images that have landscape as an aspect ratio

Mongo playground

Item properties sample
{
  "type": "image",
  "title": "",
  "id": "3458764513842250250",
  "parentId": "3458764513842634009",
  "origin": "center",
  "relativeTo": "parent_top_left",
  "createdAt": "2023-06-14T10:20:47.262Z",
  "createdBy": "3458764513823195488",
  "modifiedAt": "2023-06-22T14:26:37.422Z",
  "modifiedBy": "3458764513823195488",
  "x": 1404.0050456061917,
  "y": 1134.4583650796958,
  "width": 800,
  "height": 600,
  "rotation": 0,
  "url": "https://loremflickr.com/800/600?v=2",
  "metadata": {
    "origin": "plantuml"
  }
}
Predicate
{
  type: "image",
  $where: "this.width > this.height"
}
Any item inside a frame

Mongo playground

Item properties sample
[
  {
    "type": "image",
    "title": "",
    "id": "3458764513842250250",
    "parentId": "3458764513842634009",
    "origin": "center",
    "relativeTo": "parent_top_left",
    "createdAt": "2023-06-14T10:20:47.262Z",
    "createdBy": "3458764513823195488",
    "modifiedAt": "2023-06-22T14:26:37.422Z",
    "modifiedBy": "3458764513823195488",
    "x": 1404.0050456061917,
    "y": 1134.4583650796958,
    "width": 600,
    "height": 600,
    "rotation": 0,
    "url": "https://loremflickr.com/600/800?v=2",
    "metadata": {
      "origin": "plantuml"
    }
  },
  {
    "type": "card",
    "title": "<p>A CARD EXAMPLE</p>",
    "description": "",
    "style": {
      "cardTheme": "#2d9bf0"
    },
    "taskStatus": "none",
    "tagIds": [],
    "fields": [],
    "id": "3458764513842632919",
    "parentId": "3458764513842634009",
    "origin": "center",
    "relativeTo": "parent_top_left",
    "createdAt": "2023-06-22T14:22:29.593Z",
    "createdBy": "3458764513823195488",
    "modifiedAt": "2023-06-22T14:26:37.422Z",
    "modifiedBy": "3458764513823195488",
    "x": 2264.5268577758693,
    "y": 1064.4912441730066,
    "width": 861.145145812244,
    "height": 161.46471483979576,
    "rotation": 0,
    "metadata": {
      "status": "in_progress"
    }
  }
]
Predicate
{
  parentId: {
    $exists: true,
    $not: {
      $type: "undefined"
    },
    $nin: [
      null,
      ""
    ]
  }
}
Nested item properties

Mongo playground

Item properties sample
[
  {
    "type": "shape",
    "content": "<p>ANOTHER SHAPE</p>",
    "shape": "circle",
    "style": {
      "fillColor": "#fac710",
      "fontFamily": "open_sans",
      "fontSize": 64,
      "textAlign": "center",
      "textAlignVertical": "middle",
      "borderStyle": "normal",
      "borderOpacity": 1,
      "borderColor": "#1a1a1a",
      "borderWidth": 2,
      "fillOpacity": 1,
      "color": "#1a1a1a"
    },
    "id": "3458764513842632920",
    "parentId": "3458764513842634009",
    "origin": "center",
    "relativeTo": "parent_top_left",
    "createdAt": "2023-06-22T14:22:42.927Z",
    "createdBy": "3458764513823195488",
    "modifiedAt": "2023-06-22T14:26:37.422Z",
    "modifiedBy": "3458764513823195488",
    "x": 959.4556161563769,
    "y": 2206.9147494411936,
    "width": 735.9346151118707,
    "height": 710.7078499892141,
    "rotation": 0
  }
]
Predicate
{
  type: "shape",
  shape: "circle",
  "style.color": "#1a1a1a" 
}

contexts

contexts defines where on the board UI the custom action is available to users.
It sets the entry point for the custom action: this is the UI element that users can click to run the custom action.

Currently, custom actions are available only in the context menu of selected board items.

contexts.item

Makes the custom action available in a specific context on the board.

Custom Actions are only available in the context menu of selected board items. Currently, item is the only property supported by contexts.

The value of contexts.item is an empty object for now:

"contexts": {
  "item": {}
}