# Custom Benchmark 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:** | Property | Type | Description | |----------|------|-------------| | `$triggerName` | String | The WordPress action name this benchmark listens to | | `$actionArgNum` | Int | Number of arguments passed to the action callback. Default `1` | | `$priority` | Int | Priority for registration. Default `10` | **Abstract methods you must implement:** | Method | Returns | Description | |--------|---------|-------------| | `getBlock()` | Array | Block metadata (title, description, icon, default settings) | | `getBlockFields($funnel)` | Array | Settings fields for configuring this benchmark | | `handle($benchMark, $originalArgs)` | void | Called when the benchmark event fires — checks if the contact matches | **Built-in helper methods:** | Method | Returns | Description | |--------|---------|-------------| | `benchmarkTypeField()` | Array | Standard radio field for choosing Optional vs Required | | `canEnterField()` | Array | Standard 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 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](/modules/form-field-code-structure) 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 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`