All docs / Developer Guide

Custom Actions

Create custom actions that execute when automation recipes fire.

Creating Custom Actions

An action defines what happens when a recipe triggers and all conditions pass.

Complete Example

This example creates an action that sends a Slack notification:

<?php
// File: actions/slack-notify-action.php

if ( ! defined( 'ABSPATH' ) ) exit;

class IW_SlackNotify_Action extends IW_Automation_Action {

    /**
     * Display name in the action dropdown.
     */
    function get_title() {
        return 'Send Slack Notification';
    }

    /**
     * Restrict which triggers this action works with.
     * Omit to allow all triggers.
     */
    function allowed_triggers() {
        return array(
            'IW_Purchase_Trigger',
            'IW_OrderStatusChange_Trigger',
            'IW_OrderCreation_Trigger',
        );
    }

    /**
     * Define the admin form fields.
     */
    function get_field_schema() {
        return array(
            array(
                'type'        => 'text',
                'name'        => 'webhook_url',
                'label'       => 'Slack Webhook URL',
                'placeholder' => 'https://hooks.slack.com/services/...',
            ),
            array(
                'type'          => 'select',
                'name'          => 'channel',
                'label'         => 'Channel',
                'options'       => array(
                    array('value' => '#orders', 'label' => '#orders'),
                    array('value' => '#general', 'label' => '#general'),
                    array('value' => 'custom', 'label' => 'Custom channel...'),
                ),
                'default_value' => '#orders',
            ),
            array(
                'type'         => 'text',
                'name'         => 'custom_channel',
                'label'        => 'Custom Channel',
                'placeholder'  => '#my-channel',
                'visible_when' => array('channel' => 'custom'),
            ),
            array(
                'type'        => 'textarea',
                'name'        => 'message',
                'label'       => 'Message',
                'merge_field' => true,
                'placeholder' => 'New order {{WCOrder:OrderId}} from {{WPUser:user_email}}',
            ),
        );
    }

    /**
     * Validate configuration.
     */
    function validate_entry($config) {
        if (empty($config['webhook_url'])) {
            return 'Please enter the Slack webhook URL.';
        }
        if (!preg_match('#^https://hooks\.slack\.com/#', $config['webhook_url'])) {
            return 'Please enter a valid Slack webhook URL.';
        }
        if (empty($config['message'])) {
            return 'Please enter a message.';
        }
    }

    /**
     * Execute the action at runtime.
     *
     * @param array  $config  The saved configuration
     * @param object $trigger The trigger instance
     */
    function process($config, $trigger) {
        // IMPORTANT: Resolve merge fields before using text values
        $message = $trigger->merger->merge_text($config['message']);

        $channel = $config['channel'];
        if ($channel === 'custom') {
            $channel = $config['custom_channel'];
        }

        // Send to Slack
        wp_remote_post($config['webhook_url'], array(
            'headers' => array('Content-Type' => 'application/json'),
            'body'    => json_encode(array(
                'channel' => $channel,
                'text'    => $message,
            )),
            'timeout' => 10,
        ));
    }
}

// Register with InfusedWoo
iw_add_action_class('IW_SlackNotify_Action');

Required Methods

MethodDescription
get_title()Name shown in the admin
process($config, $trigger)Execute the action at runtime

Required for Admin Support

MethodDescription
get_field_schema()Define form fields. Without this, the action shows as “legacy” (non-editable).
validate_entry($config)Validate config. Return error string if invalid.

Optional Methods

MethodDescription
allowed_triggers()Array of trigger class names this action works with
on_class_load()Called once when the class loads. Use for hooking into frontend actions (cart modifications, etc.)

Resolving Merge Fields

Always resolve merge fields in text values before using them:

function process($config, $trigger) {
    // WRONG -- contains raw {{WPUser:first_name}}
    $name = $config['name'];

    // RIGHT -- resolved to actual value like "John"
    $name = $trigger->merger->merge_text($config['name']);
}

The merge_text() method replaces all {{Group:Key}} tokens with their actual values.

The Trigger Object

In process($config, $trigger), you can access:

Property/MethodDescription
$trigger->user_emailContact email
$trigger->wp_user_idWordPress user ID
$trigger->infusion_contact_idKeap contact ID
$trigger->pass_varsEvent data array from the trigger
$trigger->merger->merge_text($text)Resolve merge fields in a string
$trigger->search_infusion_contact_id($create)Find (or create) Keap contact

Default Values

Pre-populate fields when the action is first added to a recipe:

array('type' => 'text', 'name' => 'timeout', 'label' => 'Timeout (seconds)', 'default_value' => '30')

For dynamic defaults (e.g., from the database):

function get_field_schema() {
    $default_url = get_option('my_plugin_webhook_url', '');
    return array(
        array('type' => 'text', 'name' => 'url', 'label' => 'URL', 'default_value' => $default_url),
    );
}

Actions with Frontend Side Effects

If your action needs to modify the WooCommerce frontend (cart, checkout), use on_class_load():

function on_class_load() {
    add_action('woocommerce_before_checkout_form', array($this, 'show_banner'));
}

function process($config, $trigger) {
    // Store data in session for the frontend hook to read
    WC()->session->set('my_banner_message', $trigger->merger->merge_text($config['message']));
}

function show_banner() {
    $msg = WC()->session->get('my_banner_message');
    if ($msg) {
        echo '<div class="woocommerce-info">' . esc_html($msg) . '</div>';
    }
}

Tips

  • Keep process() fast — it runs synchronously during the WordPress request
  • For heavy processing (API calls, file operations), consider using wp_schedule_single_event() to defer
  • Use wp_remote_post() / wp_remote_get() for external API calls (not curl directly)
  • Test your action using the “Manually Trigger” feature in the recipe editor