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:

5 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.

3 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:

4 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.

4 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)?

Yes, however we are very open to merging new automation scripts the community build , what are you thinking of building?

4 Likes

Specifically, we’re looking for a way to replace some specific string in posts (we don’t care strongly about the exact syntax, but something like the plaintext @ref `Random.rand!` ) with a formatted link like Random.rand!. Looking up the exact URL is a complicated process with tens of thousands of possible targets that is completely infeasible with regexes (like Auto linkify words/watched words does) and much easier with a Turing-complete plugin-like environment… so I was curious if automations could do this.

So I was looking for a post-edit action, somewhat akin to what the @system user does when you quote the entire previous post (see here). It’d be an “Edit post” script that would trigger on “Post created” (or perhaps after-post-cooked)… but I suppose that the automations framework wouldn’t allow such a general “post-edit” action. I think it’d need to be a pretty specific link-to-Julia-docs custom automation, which certainly doesn’t make sense in a community build.

I could be barking up the wrong tree here with custom automations; I was just exploring what’s possible. Of course, as a forum for programming language enthusiasts, intrepid users are already thinking about programming bots that could use the discourse API to do this.

1 Like

I am not sure automation is what you are after here cause something that feels critical here is the “end user” experience. With automation this would only be replaced after the fact.

Thinking through this type of problem, I would probably recommend going with either a custom plugin or a theme component.

A theme component could work like so:

  1. User types: ^Rand
  2. An HTTP call is made to a backend service you host that lists all the options with URLs
  3. User selects the one they want and hit enter
  4. Markdown is swapped to [Random.rand!](https://docs.julialang.org/en/v1/stdlib/Random/#Random.rand!)

A plugin that amend the markdown pipeline could work similar to onebox and just autolink as you type leaving the original syntax. eg: ^Random.rand

I hear you on linkify not being ideal, discovery is hard, plus you may have to host a website so you normalize it to lookup.docs.julialang.org?q=Random.rand!

Certainly a very interesting problem. I think the UX of a theme component can be reasonable here.

2 Likes

That is very awesome, thank you for the thoughts and pointers here! I’ll take this to a separate topic if and when I have more questions (or answers :slight_smile: ).

1 Like

I think a custom plugin that fires on post change is what you want to create. I suspect for your use case, which has a simple trigger, it would be easier to write a plugin rather than full with the automation plugin.

1 Like