Skip to main content

Actions

An action is a script that is executed from the ftrack interface. It can be used to extend functionality in ftrack such as generating reports, launching applications or opening a custom UI. A custom action can be setup to run from inside connect by adding a hook or as a standalone script to perform company-wide operations.

Actions build on top of the Websocket events.

Example action

This example registers a new action which only shows up when a single version is selected in the ftrack interface.

import logging

import ftrack_api


class MyCustomAction(object):
'''Custom action.'''

label = 'My Action'
identifier = 'my.custom.action'
description = 'This is an example action'

def __init__(self, session):
'''Initialise action.'''
super(MyCustomAction, self).__init__()
self.session = session
self.logger = logging.getLogger(
__name__ + '.' + self.__class__.__name__
)

def register(self):
'''Register action.'''
self.session.event_hub.subscribe(
'topic=ftrack.action.discover and source.user.username={0}'.format(
self.session.api_user
),
self.discover
)

self.session.event_hub.subscribe(
'topic=ftrack.action.launch and data.actionIdentifier={0} and '
'source.user.username={1}'.format(
self.identifier,
self.session.api_user
),
self.launch
)

def discover(self, event):
'''Return action config if triggered on a single asset version.'''
data = event['data']

'''If selection contains more than one item return early since
this action can only handle a single version.'''

selection = data.get('selection', [])
self.logger.info('Got selection: {0}'.format(selection))
if (
len(selection) != 1 or
selection[0]['entityType'] != 'assetversion'
):
return

return {
'items': [{
'label': self.label,
'description': self.description,
'actionIdentifier': self.identifier
}]
}

def launch(self, event):
'''Callback method for custom action.'''
selection = event['data'].get('selection', [])

for entity in selection:

version = self.session.get(
'AssetVersion', entity['entityId']
)

'''DO SOMETHING WITH THE VERSION'''

return {
'success': True,
'message': 'Ran my custom action successfully!'
}


def register(session, **kw):
'''Register plugin.'''

'''
Validate that session is an instance of ftrack_api.Session.
If not, assume that register is being called from an incompatible API
and return without doing anything.
'''
if not isinstance(session, ftrack_api.Session):
return

action = MyCustomAction(session)
action.register()


if __name__ == '__main__':
logging.basicConfig(level=logging.INFO)
session = ftrack_api.Session()
register(session)

'''Wait for events.'''
session.event_hub.wait()
tip

To group actions together, give them the same label and specify a unique variant per action.

Action Base Class

There is also a simple reference implementation of a action base class.

It simplifies the configuration of a new action by registering with the event hub and converting the events emitted from the server to a more pythonic data representation. You can read more and find installation instructions here.

Action Service Example

The action service example is an example of how ftrack actions or event listeners can be built as a python module when they should run as a service. Multiple actions can be built into the same module if they need to share logic or are part of the same solution.

The python module can be packaged as a docker image to simplify execution and distribution. The module has a built in health check, which is used to test that the module stay in a healthy state.

User interface

When an action is launched it can respond with a few different configurations to interact with the user who launched it.

The different responses are:

Message

To respond with a message and a success value the response should look like this:

{
success: True,
message: "Ran my custom action successfully!",
type: "message"
}

Form

Actions can be setup to ask for more information through a custom UI form before being launched. To show a form before launching the action an items configuration should be returned from the launch method.

When the user has entered the requested information the launch method is called again and the event data will contain the result.

To respond with a form the response should look like this:

{
type: "form",
items: [],
title: "Foobar",
submit_button_label: "Save"
}

Where items is a list of form items that can be of different types but should all have a name to identify them. They can also have a value which will be the default value and a label to present what they mean to the user.

text

A text is a single line string that can have value, name and label.

textarea

A text area is a multiline string that can have value, name and label.

number

A number can have a value, name and label.

boolean

A boolean can have a value, name and label. It can be either True or False.

date

A date can have a value that should be in ISO format, name and label.

enumerator

An enumerator is a dropdown menu that allows for selecting between different options. The enumerator field can have a value, a name and a label but should also have a data config specifying the options to choose from.

{
 label: "My Enumerator",
type: "enumerator",
name: "my_enumerator",
data: [
{
label: "Option 1",
value: "opt1"
}, {
label: "Option 2",
value: "opt2"
}
]
}

Enumerator can either be multi-select or single-select (default). An optional key multi_select can be set to True to allow multi-selection.

label

A label does not allow any input from the user but can be used to display information. The label supports Markdown but should not have a name or label, only a value.

hidden

A hidden field that is not visible to the user. It can be used to pass around data to keep the action script implementation stateless.

Example

import logging
import datetime
import ftrack_api

class MyCustomAction(object):
'''Custom action.'''

label = 'My Action'
identifier = 'my.custom.action'
description = 'This is an example action returning UI'

def __init__(self, session):
'''Initialise action.'''
super(MyCustomAction, self).__init__()
self.session = session
self.logger = logging.getLogger(
__name__ + '.' + self.__class__.__name__
)

def register(self):
'''Register action.'''
self.session.event_hub.subscribe(
'topic=ftrack.action.discover and source.user.username={0}'.format(
self.session.api_user
),
self.discover
)
self.session.event_hub.subscribe(
'topic=ftrack.action.launch and data.actionIdentifier={0} and '
'source.user.username={1}'.format(
self.identifier,
self.session.api_user
),
self.launch
)

def discover(self, event):
'''Return action config if triggered on a single asset version.'''
data = event['data']
'''
If selection contains more than one item return early since
this action can only handle a single version.
'''
selection = data.get('selection', [])
self.logger.info('Got selection: {0}'.format(selection))
if len(selection) != 1 or selection[0]['entityType'] != 'assetversion':
return
return {
'items': [{
'label': self.label,
'description': self.description,
'actionIdentifier': self.identifier
}]
}

def launch(self, event):
if 'values' in event['data']:
values = event['data']['values']
self.logger.info(u'Got values: {0}'.format(values))
return {
'success': True,
'message': 'Ran my custom action successfully!'
}

return {
'items': [
{
'label': 'My String',
'type': 'text',
'value': 'no string',
'name': 'my_string'
}, {
'label': 'My String2',
'type': 'text',
'value': 'no string2',
  'name': 'my_string2'
}, {
'label': 'My Date',
'type': 'date',
'name': 'my_date',
'value': datetime.date.today().isoformat()
}, {
'label': 'My Number',
'type': 'number',
'name': 'my_number',
'empty_text': 'Type a number here...'
}, {
'value': '## This is a label. ##',
'type': 'label'
}, {
'label': 'Enter your text',
'name': 'my_textarea',
'value': 'some text',
'type': 'textarea'
}, {
'label': 'My Boolean',
'name': 'my_boolean',
'value': True,
'type': 'boolean'
}, {
'value': 'This field is hidden',
'name': 'my_hidden',
'type': 'hidden'
}, {
'label': 'My Enum',
'type': 'enumerator',
'name': 'my_enumerator',
'data': [
{
'label': 'Option 1',
'value': 'opt1'
}, {
'label': 'Option 2',
'value': 'opt2'
}
]
}
]
}

def register(session, **kw):
'''Register plugin.'''
if not isinstance(session, ftrack_api.Session):
return

action = MyCustomAction(session)
action.register()

if __name__ == '__main__':
logging.basicConfig(level=logging.INFO)
session = ftrack_api.Session()
register(session)
session.event_hub.wait()

Widget

A widget is a html page shown in an iframe. Read more about how to create one in Widgets. Custom widgets are the most powerful type of response since they allow a fully custom user interface.

To respond with a custom widget the response should look like this:

{
type: "widget",
url: "http://some-url/to/your/widget.html",
title: "Foobar"
}

When the widget is loaded and receive the ftrack.widget.load event, it will look like this:

{
topic: "ftrack.widget.load",
data: {
selection: [
{ id: "TASK-ID", type: "TypedContext" },
{ id: "OTHER-TASK-ID", type: "TypedContext" }
],
credentials: {
serverUrl: "https://some-server.ftrackapp.com",
apiUser: "username",
apiKey: "577ffa44-e702-47f3-b831-4c1115ebf48e"
}
}
}

Width and height

It is possible to control the width and height of a Form or Widget action by returning the width and height (in pixels):

{
items: [
{
label: "My String",
type: "text",
value: "no string",
name: "my_string"
}
],
width: 1280,
height: 800
}

Trigger remotely

The action user interfaces can be triggered programmatically and will then appear in the browser window just like when a user launch an action and the response to the launch event is a user interface. Use the ftrack.action.trigger-user-interface event  to trigger a user interface.

Here are a few possible use-cases:

  • Warn the user that the project they are logging time to is not active.
  • Ask the user if they want to approve the task when a version is approved.
  • Ask the user if they want to generate a folder on disk for the new shot that they have created.

Here is an example of sending a message to a user based on a triggered event:

def callback(event):
'''Event callback.'''
data = event['data']
user_id = event['source']['user']['id']
entities = data.get('entities', [])

for entity in entities:
if entity['entityType'] == 'timelog':
timelog = session.get('Timelog', entity['entityId'])
event = ftrack_api.event.base.Event(
topic='ftrack.action.trigger-user-interface',
data={
'type': 'message',
'success': True,
'message': 'Good job adding a timelog!'
},
target=(
'applicationId=ftrack.client.web and user.id={0}'.format(user_id)
)
)
session.event_hub.publish(event)

session.event_hub.subscribe('topic=ftrack.update', callback)
session.event_hub.wait()