Create custom Automations

:information_source: This is a draft, and may need some extra work.

Vocabulary

  • trigger: represents the name of the trigger, eg: user_added_to_group
  • triggerable: represents the code logic associated to a trigger, eg: triggers/user_added_to_group_.rb
  • script: represents the name of the script, eg: send_pms
  • scriptable: represents the code logic associated to a script, eg: scripts/send_pms.rb

Plugin API

add_automation_scriptable(name, &block)
add_automation_triggerable(name, &block)

Scriptable API

field

field :name, component: lets you add a customisable value in your automation’s UI.

List of valid components:

# foo must be unique and represents the name of your field.

field :foo, component: :text # generates a text input
field :foo, component: :list # generates a multi select text input where users can fill values
field :foo, component: :choices, extra: { content: [ {id: 1, name: 'your.own.i18n.key.path' } ] } # generates a combo-box with a custom content
field :foo, component: :boolean # generate a checkbox input
field :foo, component: :category # generates a category-chooser
field :foo, component: :group # generates a group-chooser
field :foo, component: :date_time # generates a date time picker
field :foo, component: :tags # generates a tag-chooser
field :foo, component: :user  # generates a user-chooser
field :foo, component: :pms  # allows to create one or more PM templates
field :foo, component: :categories  # allows to select zero or more categories
field :foo, component: :key-value  # allows to create key-value pairs
field :foo, component: :message  # allows to compose a PM with replaceable variables
field :foo, component: :trustlevel  # allows to select one or more trust levels
triggerables and triggerable!
# Lets you define the list of triggerables allowed for a script
triggerables %i[recurring]

# Lets you force a triggerable for your script and also lets you force some state on fields
field :recurring, component: :boolean
triggerable! :recurring, state: { foo: false }
placeholders
# Lets you mark a key as replaceable in texts using the placeholder syntax `%%sender%%`
placeholder :sender

Note that it’s the responsibility of the script to provide values for placeholders and to apply the replacement using input = utils.apply_placeholders(input, { sender: 'bob' })

script

This is the heart of an automation and where all the logic happens.

# context is sent when the automation is triggered, and can differ a lot between triggers
script do |context, fields, automation|
end

Localization

Each field you will use will depend on i18n keys and will be namespaced to their trigger/script.

For example a scriptable with this content:

field :post_created_edited, component: :category

Will require the following keys in client.en.yml:

en:
  js:
    discourse_automation:
      scriptables:
        post_created_edited:
          fields:
            restricted_category:
              label: Category
               description: Optional, allows to limit trigger execution to this category

Note that description is optional here.

4 Likes

When I saw this was a new topic I got excited: I thought more details had been shared! :laughing:

As someone who does not program in Ruby, but very interested in workflow automation, I was hoping I might grok a little bit more by example…

:thinking:

Guess I’ll need to start at Developing Discourse Plugins - Part 1 - Create a basic plugin… :sweat_smile:

3 Likes

I essentially just chopped this section from the plugin topic so it didn’t appear as if you needed to know it to make use of the existing ones. :slight_smile:

I agree that it would be great if this was a little bit more of a step-by-step. I’ve sent out a flare for community assistance here to see if anyone has experience of such things: :crossed_fingers:

3 Likes

A hello world example would be cool.
Where should the scripts be stored? Would like to experiment with it a bit.

2 Likes

I think you can write custom scripts using Chat GPT along with this plugin.

Probably the best place to start looking is in the automation script that’s added to the Data Explorer plugin: https://github.com/discourse/discourse-data-explorer/blob/main/plugin.rb#L82-L122. It’s also worth looking at the Automation plugin’s existing scripts and triggers: https://github.com/discourse/discourse-automation/tree/main/lib/discourse_automation

Since there’s not much information on Meta about adding custom automations, here’s an example plugin.rb file that adds a script to update a user’s Activity Summary email preference. The script can be triggered by the Automation plugin’s ‘user_added_to_group’ or ‘user_removed_from_group’ triggers.

# frozen_string_literal: true

# name: automation-script-example
# about: An example of how to add a script to an automation
# version: 0.0.1
# authors: scossar

enabled_site_setting :automation_script_example_enabled

after_initialize do
  reloadable_patch do
    if defined?(DiscourseAutomation)
      DiscourseAutomation::Scriptable::USER_UPDATE_SUMMARY_EMAIL_OPTIONS =
        "user_update_summary_email_options"
      add_automation_scriptable(
        DiscourseAutomation::Scriptable::USER_UPDATE_SUMMARY_EMAIL_OPTIONS
      ) do

        field :email_digests, component: :boolean

        version 1
        triggerables [:user_added_to_group, :user_removed_from_group]

        script do |context, fields, automation|
          if automation.script == "user_update_summary_email_options" && (context["kind"] == "user_added_to_group" || context["kind"] == "user_removed_from_group")
            user_id = context["user"].id
            digest_option = fields.dig("email_digests", "value")
            user_option = UserOption.find_by(user_id: user_id)

            if (user_option)
              user_option.update(email_digests: digest_option)
            end
          end
        end
      end
    end
  end
end

The full plugin code is here: GitHub - scossar/automation-script-example: An example of how to add a custom script to the Discourse Automation plugin..

:warning: please don’t use this code as it is on a production site. I hadn’t looked at the Automation code before this evening. If I get any feedback about potential issues with the code, I’ll update this post and the GitHub repo.

Edit: my concern was how to best deal with the case of multiple automation scripts being triggered by either the ‘user_added_to_group’ or ‘user_removed_from_group’ triggers. The initial version of the plugin was checking for:

fields.has_key?("email_digests")

but that felt kind of flaky. What if another script was added that also had an email_digests key?

The updated code passes the automation parameter to the code block and checks:

automation.script == "user_update_summary_email_options"

That should ensure that the script won’t be run for the wrong automation.

… thinking about it some more, it’s unlikely the script could get triggered by an automation it wasn’t configured for :slight_smile:

3 Likes

I’d like to know this too - once you’ve made a repo like @simon’s, how does it get accessed by the plugin?

Do we have to fork the whole plugin and drop it in with the existing ones in https://github.com/discourse/discourse-automation/tree/main/lib/discourse_automation/scripts? Or is there a more elegant way?

You need to install it like any other Discourse plugin: Install Plugins in Discourse. So you would install the Automation plugin and install your plugin that adds the custom scripts. The reason it works is because of the methods defined here: https://github.com/discourse/discourse-automation/blob/main/lib/plugin/instance.rb. In the example code I posted above, you’ll see that the custom script is being added with a call to add_automation_scriptable.

Note: don’t install the example automation from my github repo, just take it as an example of how to extend the Automation plugin. (I forgot I’d linked to it here and updated it so that it only works with my forked version of the Discourse Automation plugin. The code I linked to here is still valid though: Create custom Automations - #6 by simon. I’ll update the automation-script-example plugin ASAP so that it works without the changes I made to my forked version of the Automation plugin.)

My concern was unfounded. This condition isn’t necessary:

if automation.script == "user_update_summary_email_options" && (context["kind"] == "user_added_to_group" || context["kind"] == "user_removed_from_group")

I’ll update the example soon.

3 Likes

Am I correct in understanding that custom automations require a self-hosted installation (or otherwise direct backend access to the filesystem where Discourse is installed)?