$npx -y skills add Lonsdale201/wp-agent-skills --skill fluentcrm-funnel-benchmarkBuild a custom FluentCRM funnel benchmark — a goal/wait point
| 1 | # FluentCRM: register a custom funnel benchmark |
| 2 | |
| 3 | For developers building a funnel **wait point** — a node placed inside an automation that pauses the contact until a specific event matches the configured criteria (tag applied, course completed, payment received, custom event from your plugin). Unlike a trigger (which STARTS a funnel) or an action (which DOES something), a benchmark *gates* progress mid-flow. Extends `FluentCrm\App\Services\Funnel\BaseBenchMark`. Verified against FluentCRM 3.1.8. |
| 4 | |
| 5 | ## API stability note |
| 6 | |
| 7 | `BaseBenchMark`, the registration triplet (`fluentcrm_funnel_blocks` + `fluentcrm_funnel_block_fields` + `fluentcrm_funnel_benchmark_start_{name}`), the `assertCurrentGoalState` filter, and `FunnelProcessor::startFunnelFromSequencePoint()` have been stable since FluentCRM 2.6.0 (when goal-state assertion was introduced). The Optional vs Essential semantics and the `can_enter` direct-entry mechanism are part of that 2.6 baseline. |
| 8 | |
| 9 | ## Misconception this skill corrects |
| 10 | |
| 11 | > "Benchmarks are special wait nodes — they have their own action listener separate from triggers." |
| 12 | |
| 13 | Wrong. Benchmarks listen on the **same** WP action as triggers — `FunnelHandler::mapTriggers()` handles both in one pass. After dispatching `fluentcrm_funnel_start_{triggerName}` for matching trigger funnels, it queries `FunnelSequence` for benchmark sequences with `action_name === $triggerName` whose funnel is published, and dispatches `fluentcrm_funnel_benchmark_start_{triggerName}` for each. |
| 14 | |
| 15 | Practical consequences: |
| 16 | |
| 17 | 1. **The same `fluentcrm_funnel_arg_num_{name}` filter timing rule applies.** Instantiate on `fluentcrm_loaded` priority below 10. Hook on `fluent_crm/after_init` and your benchmark misses both active-trigger listener passes; hook too late in `init` and events fired by other `init` callbacks can be missed or reduced to the default one accepted arg. |
| 18 | 2. **`triggerName` collision is fine** between a trigger and a benchmark — they coexist on the same action. `TagAppliedBenchmark::triggerName = 'fluentcrm_contact_added_to_tags'` is used by both the trigger flow ("Tag Applied" trigger that starts a funnel) and the benchmark flow ("Tag Applied" wait point). |
| 19 | 3. **`fluentcrm_funnel_settings` option lifecycle includes benchmarks.** `resetFunnelIndexes()` queries published-funnel benchmark sequences — saving a funnel that uses your benchmark adds the trigger name to the listener registry. |
| 20 | |
| 21 | Other AI-prone misconceptions: |
| 22 | |
| 23 | - **"`triggerName` on a benchmark is the benchmark's identifier."** Wrong. `triggerName` is the WP action name the benchmark listens for — same semantics as `BaseTrigger::triggerName`. Pick a hook your plugin already fires (or an existing FluentCRM contact-state hook like `fluentcrm_contact_added_to_tags`); don't invent a name unique to the benchmark unless you're also firing `do_action('your_name', ...)` from a dispatcher. |
| 24 | - **"`getBlock()` doesn't need a `'settings'` key."** Same trap as BaseAction. The `'settings'` hash on the `getBlock()` return is the seed for new instances dragged into the funnel; the editor's Vue components bind directly to `settings.<field_key>`. Omit it and dragging the block in throws **`TypeError: Cannot read properties of undefined (reading '<your_first_field>')`** in `start.js`, leaving the panel empty. `BaseBenchMark::addBenchmark` at [BaseBenchMark.php:53-62](BaseBenchMark.php) does NOT inject defaults. Seed every field key, including the auto-rendered `type` (Optional/Essential) and `can_enter` (direct-entry) when you reference them via `$this->benchmarkTypeField()` / `$this->canEnterField()`. |
| 25 | - **"`handle()` on a benchmark is identical |