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
- A custom action in the context menu can make it easier for users to streamline their workflows. For example:
- 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 theindex.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
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
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
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
ui
The object exposes content that is shown to board users in the context menu of one or more selected board items.
ui.label
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
: Frenchde
: Germanja_JP
: Japanesees
: Spanishpt_BR
: Brazilian Portuguese
ui.icon
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
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
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
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
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
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
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
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
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
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
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
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
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": {}
}