$npx -y skills add Lonsdale201/wp-agent-skills --skill fluentcrm-funnel-triggerBuild a custom FluentCRM funnel trigger by extending
| 1 | # FluentCRM: register a custom funnel trigger |
| 2 | |
| 3 | For developers building a companion plugin that needs to start a FluentCRM automation when something happens (course enrolled, SaaS webhook arrived, custom CPT published, third-party event). Extends `FluentCrm\App\Services\Funnel\BaseTrigger` and registers via the `fluentcrm_funnel_triggers` filter chain. This skill maps the contract verified end-to-end against FluentCRM 3.1.8 source, with the lifecycle order written out in full because **timing is where this contract silently breaks**. |
| 4 | |
| 5 | ## API stability note |
| 6 | |
| 7 | The `BaseTrigger` abstract class, the `fluentcrm_funnel_triggers` / `fluentcrm_funnel_start_*` / `fluentcrm_funnel_editor_details_*` / `fluentcrm_funnel_arg_num_*` hook family, and the `FunnelProcessor::startFunnelSequence()` entry point have been stable across the 2.7+ line. In FluentCRM 3.1.8, active trigger listeners are registered by `FunnelHandler::registerEarlyActiveTriggers()` on `init:2` when the arg-count filter is already present, with a fallback `registerActiveTriggers()` pass on `init:20`. |
| 8 | |
| 9 | ## Misconception this skill corrects |
| 10 | |
| 11 | > "I'll register my trigger on `fluent_crm/after_init` like the other addons do — it's a clean post-boot hook." |
| 12 | |
| 13 | Wrong hook. **`fluent_crm/after_init` runs on `init` priority 1000** ([boot/app.php](../../../../wp-content/plugins/fluent-crm/boot/app.php)). In 3.1.8, FluentCRM's `FunnelHandler` registers core funnel items on `init:1`, then performs an early active-trigger listener pass on `init:2` and a fallback pass on `init:20`. The early pass only registers trigger listeners whose `fluentcrm_funnel_arg_num_{name}` filter already exists. By the time `fluent_crm/after_init` fires, both listener passes have already run. |
| 14 | |
| 15 | The visible bug: a trigger like `lw_lms_after_grant` (which fires with 5 args) either misses events fired during `init:10`, or gets only `$user_id` delivered if the fallback listener captured the default arg count. `$courseId = (int) ($originalArgs[1] ?? 0)` becomes `0`. Any guard `if ($courseId <= 0) return;` silently drops every event. |
| 16 | |
| 17 | The fix is one line: instantiate your `BaseTrigger` subclass on `fluentcrm_loaded` priority 5 instead of `fluent_crm/after_init`. FluentCampaign Pro registers its funnel items on `init:1`, which lands before the `init:2` early active-trigger pass. |
| 18 | |
| 19 | Other AI-prone misconceptions: |
| 20 | |
| 21 | - **"I'll fire `fluentcrm_funnel_start_{my_trigger}` directly to start the funnel."** No. That action is dispatched by `FunnelHandler::mapTriggers()` after it has already validated that a published funnel matches the trigger name. Calling it directly bypasses the funnel lookup, the per-funnel sequence config, and the benchmark dispatch. **Always go through `(new FunnelProcessor())->startFunnelSequence($funnel, $subscriberData, [...])`** at the end of your `handle()` method. |
| 22 | - **"`triggerName` is a label."** It is the **literal WP action hook name**. FluentCRM's `registerActiveTriggers()` does `add_action($triggerName, ...)`. Either set `triggerName` to a real WP action that already fires (e.g. `tutor_after_enrolled`), or pick a custom name and fire `do_action('my_custom_event', $arg1, $arg2)` yourself from a bridge — see "Custom-name pattern" below. |
| 23 | - **"FluentCRM listens to my hook the moment my plugin loads."** Wrong. FluentCRM only adds a listener for trigger names present in the `fluentcrm_funnel_settings` WP option. That option is rebuilt by `resetFunnelIndexes()`, which runs when a funnel is created / updated / status-changed via `FunnelController`. **No published funnel using your trigger name → no listener → your trigger silently doesn't fire.** When you ship a |