Developer Docs
Build Custom Actions

Build Custom Actions

Actions provide the key functionality for the ProPresenter Triggers application. Within the ./actions/ directory of the app you'll find the files that define the classes used to create the actions' functionality.

Actions at their core are just Python classes. The constructor instantiates the class as an object and sets the initial state. The run() method is called when the action is triggered.

A future version will rework actions to require values to be passed directly into the run() method. This will allow for more flexibility and will allow actions to be used in more ways such as for defining listeners. After this change, constructors will no longer be used to pass in state for each action call.

Abstract Action Class

The Action class is an abstract class that all actions must inherit from. This class provides the outline for the action's functionality. The Action class is defined in the ./actions/Action.py file. The Action class has two methods that must be implemented by the action's class:

  • __init__: The constructor for the action's class. This method is called when the action is instantiated. The constructor can accept parameters that are passed in from the trigger using the value parameter. The constructor can also be used to hard code any initial state needed by the action.
  • run: The method that is called when the action is triggered. This method doesn't currently accept any parameters. This method is called when the action is triggered.

State

Actions receive state from PTDL trigger values via a parameter. This state is a string that can be a single value or a JSON object. It's up to the action to parse the state and use it as needed. Here's an example:

import json
class Example:
    def __init__(self, value):
        # Parse json value
        self.value = json.loads(value)

In addition to the action's parameter, actions can also get data from the config.json file in the app's root directory. This is useful for storing data that is used across multiple instances of an action such as:

  • An API key used to call an endpoint
  • A magic value used within the action's logic
  • etc...

To use a config data import the import_config() method from the utils module. This will return a dictionary of the config data. The dictionary can then be accessed like any other dictionary.

from app.utils import import_config

Then call the method from your code. For example:

self.config = import_config().get('actions')['Example'] or ""

Logging

This project uses the logging package to log messages to the console. The logging package is part of the Python standard library and is used to log messages to the console. The logging package has a number of built-in methods for logging messages at different levels. The levels are:

  • DEBUG: Detailed information, typically of interest only when diagnosing problems.
  • INFO: Confirmation that things are working as expected.
  • WARNING: An indication that something unexpected happened, or indicative of some problem in the near future (e.g. ‘disk space low’). The software is still working as expected.
  • ERROR: Due to a more serious problem, the software has not been able to perform some function.
  • CRITICAL: A serious error, indicating that the program itself may be unable to continue running.
⚠️

Logging is great for sending logs to the console, but should not be used for crticial or fatal exceptions. See the Exceptions section for more information.

As normal you can import the logging package then call the logging methods to log messages. For example:

import logging
logging.debug('This is a debug message')
logging.info('This is an info message')
logging.warning('This is a warning message')
logging.error('This is an error message')
logging.critical('This is a critical message')

Exceptions

Exceptions must be raised up to the calling function and not caught within the class. By throwing or rasing excptions up past the action's class, the app can catch the exception and handle it appropriately. If an exception is not raised to the calling function, the action may result in unanticipated behavior when using sequenced or chained triggers.

This doesn't mean that you can't use try/except blocks. Instead, just make sure that you raise the exception up to the calling function if you can't handle it automatically within the class.

To simplify this process, the utils module includes an exception class called ActionError. This class can be imported and used to raise exceptions related to actions. For example:

from app.utils import ActionError
 
raise ActionError('This is an exception message')

ActionError takes a single parameter which is the exception message. This message will be logged to the console and displayed to the user in the app as an error. Additionally, any chained or sequenced triggers will be stopped.

When should you not raise an exception? If you're using a try/except block to catch an exception and can fully handle it within the action, you should not raise the exception. Instead, you should handle the exception within the try/except block. Only raise an exception past your class when the class experiences a fatal or crticial error that cannot be handled within the class.

By rasing fatal or critical errors above the class level users can be notified of the error and the app can stop any chained or sequenced triggers.

Example Action

Within the ./actions/ directory you'll find an example action called Example.py. This action is a good starting point for creating your own actions. It has a number of comments that explain how the action works.

Find the most up to date version here (opens in a new tab) and/or copy the code below:

Example.py
import logging
from app.actions.Action import Action
from app.utils import import_config
from app.utils import ActionError
 
 
class Example(Action):
    """
    Class name: Example
    Class description: Example class showing the capabilities
        of the plugin system including getting data from the
        config.json file and accepting parameters.
    """
 
    def __init__(self, value='Default Thingy'):
        """
        Method name: __init__
        Method description: Constructor for Example class
        @param value: value to print
        """
        # Use the value passed in as a parameter (if any) or the default value
        self.value = value
 
        # See how to throw an exceptions by setting the value to "Test" or "Ronald"
        if self.value == "Test" or self.value == "Ronald":
            # Demonstrate how to raise an exception
            if self.value == "Ronald":
                raise ActionError("Ronald isn't allowed")
 
            # Demonstrate try/except
            try:
                # this will always fail
                print(1 / 0)
            except Exception as e:
                # You can log to the server console
                logging.info('You can\'t divide by zero')
                # Then raise an exception to block the action
                raise ActionError('Example try/except error: ' + str(e))
 
        # Get the config value from the config.json file or an empty string
        self.config = import_config().get('actions')['Example'] or ""
 
    def run(self):
        """
        Method name: run
        Method description: Runner method that prints "Hello from Example action!" plus the value and config
        """
        # Print to the console
        print('Hello from Example action!\n' + self.value + "\n" + self.config)
 
        # Log to the server console
        logging.info('Hello from Example action! ' + self.value + " " + self.config)
 

Using Your Action

This project uses a markup language called ProPresenter 3rd Party Trigger Definition Language (PTDL) which is designed for allowing users of ProPresenter to define triggers using slide and presentation notes.

Your action's key is the name of the file/class and the value can be any string. The string will be passed to the action's constructor as a parameter.

Submitting Actions

You're welcome to use your custom actions locally simply by placing your action's file in the ./actions/ directory and making calls using PTDL, but if you'd like to submit them to be included in the ProPresenter Triggers app for other users to use, please submit a pull request to the GitHub repo (opens in a new tab).

All submissions will be reviewed and if relevant and appropriate will be included in the app.

Any code submitted to the repo will be licensed under the project's license (opens in a new tab), so make sure you have permission to submit the code before doing so.