An action is a custom 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 Events framework.

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):
        # Exit to avoid registering this plugin again.
        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.

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
  • Form
  • Widget

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'
}

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.

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

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 datetimeimport ftrack_apiclass 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']:
            # Do something with the values or return a new form.
            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.'''    # 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):
        # Exit to avoid registering this plugin again.
        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()

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'
        }
    }
}

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  (link) 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)# Subscribe to the update topic.
session.event_hub.subscribe('topic=ftrack.update', callback)
session.event_hub.wait()

Did this answer your question?