Skip to content
View as Markdown

Custom Benchmark ​

FluentCRM Core Intermediate

A benchmark is a goal or checkpoint within an automation funnel. Unlike triggers (which start a funnel) and actions (which execute tasks), benchmarks act as conditional gates — the automation pauses until the contact meets the benchmark criteria.

For example, "Tag Applied" or "Link Clicked" are benchmarks. Contacts can also enter a funnel directly at a benchmark point if configured.

Benchmarks have two types:

  • Optional — The automation continues past this point without waiting. If the benchmark fires later, the contact jumps to this point.
  • Required (Essential) — The automation waits at this point until the contact meets the goal before continuing.

Base Class ​

Extend FluentCrm\App\Services\Funnel\BaseBenchMark and implement the required abstract methods.

Properties:

PropertyTypeDescription
$triggerNameStringThe WordPress action name this benchmark listens to
$actionArgNumIntNumber of arguments passed to the action callback. Default 1
$priorityIntPriority for registration. Default 10

Abstract methods you must implement:

MethodReturnsDescription
getBlock()ArrayBlock metadata (title, description, icon, default settings)
getBlockFields($funnel)ArraySettings fields for configuring this benchmark
handle($benchMark, $originalArgs)voidCalled when the benchmark event fires — checks if the contact matches

Built-in helper methods:

MethodReturnsDescription
benchmarkTypeField()ArrayStandard radio field for choosing Optional vs Required
canEnterField()ArrayStandard checkbox for allowing direct entry at this point

Step-by-Step Example ​

Let's create a benchmark that triggers when specific tags from your plugin are applied to a contact.

1. Constructor ​

php
<?php

namespace YourPlugin\Automation;

use FluentCrm\App\Services\Funnel\BaseBenchMark;
use FluentCrm\App\Services\Funnel\FunnelProcessor;
use FluentCrm\Framework\Support\Arr;

class TagAppliedBenchmark extends BaseBenchMark
{
    public function __construct()
    {
        $this->triggerName = 'your_plugin_tag_applied';
        $this->actionArgNum = 2;
        $this->priority = 20;
        parent::__construct();
    }
}

The parent constructor registers:

  • addBenchmark() on the fluentcrm_funnel_blocks filter
  • pushBlockFields() on the fluentcrm_funnel_block_fields filter
  • handle() on the fluentcrm_funnel_benchmark_start_{triggerName} action
  • assertCurrentGoalState() on the fluent_crm/benchmark_already_asserted_{triggerName} filter

2. getBlock() ​

Return block metadata. Note the settings key must include defaults for type (benchmark type) and can_enter:

php
public function getBlock()
{
    return [
        'title'       => __('My Plugin Tag Applied', 'your-plugin'),
        'description' => __('This will run when selected tags are applied to a contact', 'your-plugin'),
        'icon'        => 'fc-icon-apply_list',
        'settings'    => [
            'tags'        => [],
            'select_type' => 'any',
            'type'        => 'optional',
            'can_enter'   => 'yes',
        ],
    ];
}

3. getBlockFields() ​

Define the settings UI. Use the built-in benchmarkTypeField() and canEnterField() helpers for the standard benchmark controls:

php
public function getBlockFields($funnel)
{
    return [
        'title'     => __('My Plugin Tag Applied', 'your-plugin'),
        'sub_title' => __('This will run when selected tags are applied to a contact', 'your-plugin'),
        'fields'    => [
            'tags' => [
                'type'        => 'multi-select',
                'options'     => $this->getTagOptions(),
                'is_multiple' => true,
                'label'       => __('Select Tags', 'your-plugin'),
                'placeholder' => __('Select Tags', 'your-plugin'),
            ],
            'select_type' => [
                'label'   => __('Run When', 'your-plugin'),
                'type'    => 'radio',
                'inline'  => true,
                'options' => [
                    ['id' => 'any', 'title' => __('Contact has any of the selected tags', 'your-plugin')],
                    ['id' => 'all', 'title' => __('Contact has all of the selected tags', 'your-plugin')],
                ],
                'dependency' => [
                    'depends_on' => 'tags',
                    'operator'   => '!=',
                    'value'      => '',
                ],
            ],
            'type'      => $this->benchmarkTypeField(),
            'can_enter' => $this->canEnterField(),
        ],
    ];
}

See Form Field Types for all available field types.

4. handle() ​

Called when the benchmark event fires. Check if the contact matches the benchmark criteria, and if so, start or resume the funnel from this point:

php
public function handle($benchMark, $originalArgs)
{
    $tagIds = $originalArgs[0];
    $subscriber = $originalArgs[1];
    $settings = $benchMark->settings;

    // Quick check: do any applied tags overlap with configured tags?
    $isMatched = array_intersect($settings['tags'], $tagIds);
    if (!$isMatched) {
        return false;
    }

    $matchType = Arr::get($settings, 'select_type');
    $subscriberTags = $subscriber->tags->pluck('id')->toArray();
    $intersection = array_intersect($settings['tags'], $subscriberTags);

    if ($matchType === 'any') {
        $isMatched = !empty($intersection);
    } else {
        // All configured tags must be present
        $isMatched = count($intersection) === count($settings['tags']);
    }

    if (!$isMatched) {
        return false;
    }

    $funnelProcessor = new FunnelProcessor();
    $funnelProcessor->startFunnelFromSequencePoint($benchMark, $subscriber);
}

5. Fire the Benchmark Event ​

In your plugin, fire the action when the event occurs:

php
// When tags are applied in your plugin:
do_action('your_plugin_tag_applied', $tagIds, $subscriber);

6. Register ​

php
add_action('fluent_crm/after_init', function () {
    new YourPlugin\Automation\TagAppliedBenchmark();
});

Complete Code ​

php
<?php

namespace YourPlugin\Automation;

use FluentCrm\App\Services\Funnel\BaseBenchMark;
use FluentCrm\App\Services\Funnel\FunnelProcessor;
use FluentCrm\Framework\Support\Arr;

class TagAppliedBenchmark extends BaseBenchMark
{
    public function __construct()
    {
        $this->triggerName = 'your_plugin_tag_applied';
        $this->actionArgNum = 2;
        $this->priority = 20;
        parent::__construct();
    }

    public function getBlock()
    {
        return [
            'title'       => __('My Plugin Tag Applied', 'your-plugin'),
            'description' => __('This will run when selected tags are applied to a contact', 'your-plugin'),
            'icon'        => 'fc-icon-apply_list',
            'settings'    => [
                'tags'        => [],
                'select_type' => 'any',
                'type'        => 'optional',
                'can_enter'   => 'yes',
            ],
        ];
    }

    public function getBlockFields($funnel)
    {
        return [
            'title'     => __('My Plugin Tag Applied', 'your-plugin'),
            'sub_title' => __('This will run when selected tags are applied to a contact', 'your-plugin'),
            'fields'    => [
                'tags' => [
                    'type'        => 'multi-select',
                    'options'     => $this->getTagOptions(),
                    'is_multiple' => true,
                    'label'       => __('Select Tags', 'your-plugin'),
                    'placeholder' => __('Select Tags', 'your-plugin'),
                ],
                'select_type' => [
                    'label'   => __('Run When', 'your-plugin'),
                    'type'    => 'radio',
                    'inline'  => true,
                    'options' => [
                        ['id' => 'any', 'title' => __('Contact has any of the selected tags', 'your-plugin')],
                        ['id' => 'all', 'title' => __('Contact has all of the selected tags', 'your-plugin')],
                    ],
                    'dependency' => [
                        'depends_on' => 'tags',
                        'operator'   => '!=',
                        'value'      => '',
                    ],
                ],
                'type'      => $this->benchmarkTypeField(),
                'can_enter' => $this->canEnterField(),
            ],
        ];
    }

    public function handle($benchMark, $originalArgs)
    {
        $tagIds = $originalArgs[0];
        $subscriber = $originalArgs[1];
        $settings = $benchMark->settings;

        $isMatched = array_intersect($settings['tags'], $tagIds);
        if (!$isMatched) {
            return false;
        }

        $matchType = Arr::get($settings, 'select_type');
        $subscriberTags = $subscriber->tags->pluck('id')->toArray();
        $intersection = array_intersect($settings['tags'], $subscriberTags);

        if ($matchType === 'any') {
            $isMatched = !empty($intersection);
        } else {
            $isMatched = count($intersection) === count($settings['tags']);
        }

        if (!$isMatched) {
            return false;
        }

        $funnelProcessor = new FunnelProcessor();
        $funnelProcessor->startFunnelFromSequencePoint($benchMark, $subscriber);
    }

    private function getTagOptions()
    {
        // Replace with your plugin's tag query
        return [
            ['id' => 'beginner', 'title' => 'Beginner'],
            ['id' => 'advanced', 'title' => 'Advanced'],
        ];
    }
}

Source: app/Services/Funnel/BaseBenchMark.php