Fluentcrm overview
Skill Lonsdale201/wp-agent-skills/fluentcrm/fluentcrm-overview
A community-maintained collection of agent skills for WordPress plugin and theme development.
npx -y skills add Lonsdale201/wp-agent-skills --skill fluentcrm-overviewAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 21 stars21 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Orient skill for FluentCRM extension development. Covers the Free / Pro split (FluentCRM = funnel chassis; FluentCampaign Pro = integrations + advanced actions / benchmarks), plugin paths and constants, the bootstrap order (fluentcrm_loaded → fluentcrm_addons_loaded → init funnel listener passes → fluent_crm/after_init), the model layer (Subscriber, Company, EventTracker, Funnel, FunnelSequence, FunnelSubscriber, FunnelMetric), the global helpers (FluentCrmApi, fluentCrmDb, FunnelHelper), the contact lifecycle hooks (fluent_crm/contact_created, _updated, _email_changed, _custom_data_updated), the smart-code extension filter (fluent_crm/extended_smart_codes), and a decision matrix for picking the right extension contract. Use when scaffolding a new FluentCRM integration, choosing which contract to extend, or asking where things live. Triggers on FluentCrmApi, fluentCrmDb, FunnelHelper, fluent_crm/contact_, fluent_crm/extended_smart_codes, FLUENTCRM, FLUENTCAMPAIGN.
SKILL.md
25.0 KB, ~5.5k tokens by cl100k_base, as published. Nobody here has run it
FluentCRM: developer overview
Pick this skill first when starting a new FluentCRM extension or when you don't yet know which contract to extend. It's the orient layer; it doesn't teach any single extension point in depth — it lays out the file map and points you at the focused skills (fluentcrm-funnel-trigger, fluentcrm-funnel-action, fluentcrm-funnel-benchmark, fluentcrm-rest-options).
Plugin identity
| Field | Value | Source |
|---|---|---|
| Slug | fluent-crm | header |
| Version | 3.1.8 | fluent-crm.php |
| Pro slug | fluentcampaign-pro | header |
| Pro version | 3.1.8 | fluentcampaign-pro.php |
| Boot constant | FLUENTCRM ('fluentcrm') | fluent-crm.php:20 |
| Pro constant | FLUENTCAMPAIGN_PLUGIN_VERSION | fluentcampaign-pro.php |
| Path constant (Free) | FLUENTCRM_PLUGIN_PATH | fluent-crm.php:22 |
| Path constant (Pro) | FLUENTCAMPAIGN_PLUGIN_PATH | fluentcampaign-pro.php:20 |
| Min PHP (verified) | 7.4 | composer.json |
Active-detection — never test for class_exists('FluentCRM') (no such class). Use:
public static function isFluentCRMActive(): bool {
return defined('FLUENTCRM');
}
public static function isFluentCRMProActive(): bool {
return defined('FLUENTCAMPAIGN_PLUGIN_VERSION')
&& version_compare(FLUENTCAMPAIGN_PLUGIN_VERSION, '3.1.8', '>=');
}
Use a lower Pro floor only when you have verified the exact Pro API you call. Core 3.1.8 declares FLUENTCRM_MIN_PRO_VERSION as 3.1.8, so extension code that depends on current Pro internals should not silently accept older Pro builds.
Active-detection canon for OTHER plugins (load-order trap)
Triggers / actions / benchmarks register on fluentcrm_loaded:5, which fires from FluentCRM's own plugins_loaded:10 callback. Any dep check in your registrar runs at that point — and only symbols declared at FILE LOAD are guaranteed available. WordPress runs plugins_loaded:10 callbacks in plugin registration order (non-deterministic across installs), so any symbol that's only set up INSIDE another plugin's plugins_loaded callback is racy.
Safe (declared at file load — top-level scope, no conditional require):
| Plugin | Use | Why |
|---|---|---|
| WooCommerce | class_exists('WooCommerce') | woocommerce.php includes class-woocommerce.php at file scope |
| FluentCampaign Pro | defined('FLUENTCAMPAIGN_PLUGIN_VERSION') | fluentcampaign-pro.php defines the constant before bootstrapping |
| WC Memberships | class_exists('WC_Memberships_Loader') | top-level loader class, WC_Memberships itself is loaded inside init_plugin() and is racy |
| LW LMS | class_exists('LightweightPlugins\\LMS\\Plugin') | autoloader registered at file scope |
| Jet Engine | class_exists('Jet_Engine') | top-level class declaration |
Unsafe (declared inside the dep's own callback — DO NOT use at registration time):
// WRONG — wc_memberships() function is declared inside Memberships's
// plugins_loaded:10 callback. If your registrar runs before Memberships's
// callback, function_exists returns false and your action is silently
// excluded from the editor.
return function_exists('wc_memberships');
// RIGHT — WC_Memberships_Loader is at file scope.
return class_exists('WC_Memberships_Loader');
If the dep's main object only exists post-init, find a top-level loader/registrar to test against (every well-formed plugin has one).
Free vs Pro at a glance
| Capability | Free (FluentCRM) | Pro (FluentCampaign) |
|---|---|---|
| Contacts, lists, tags, segments | Yes | Yes |
| Email campaigns, sequences, templates | Yes | Yes |
| Forms, opt-ins | Yes | Yes |
| Funnel triggers (BaseTrigger) | Yes — register from any plugin | Yes — many built-in (Tutor, LearnDash, LifterLMS, EDD, WishlistMember, AffiliateWP, Woo Subscriptions) |
| Funnel actions (BaseAction) | Yes — register from any plugin | Yes — many built-in (advanced Woo, sequence add/remove, custom field updates) |
| Funnel benchmarks (BaseBenchMark) | Yes — register from any plugin | Yes — additional built-ins |
| Conditional / A-B branching node | No | Yes |
| Companies module | Yes, experimental flag company_module | Yes |
| Event tracking | Yes, experimental flag event_tracking | Adds trigger/action/conditions |
| MCP / Abilities API surface | Yes, guarded by wp_register_ability + mcp_enabled | Adds Pro abilities |
| Webhook receiver / sender | Yes | Yes |
| SMTP / mail driver layer | Free uses WP mail / Fluent SMTP | Pro adds advanced reporting |
The four extension contracts (trigger, action, benchmark, smart code) are all in Free. Pro is mostly built-in integrations + UI features. A companion plugin extending Free works against Pro automatically.
Bootstrap order
plugins_loaded:10 ← your plugin instantiates its bootstrap
↓
fluentcrm_loaded ← do_action('fluentcrm_loaded') from boot/app.php:41
REGISTER triggers / actions / benchmarks HERE (priority < 10)
FluentCampaign Pro's IntegrationHandler runs here too
↓
fluentcrm_addons_loaded ← do_action('fluentcrm_addons_loaded') from boot/app.php:42
addon boot point; Pro Application loads here
↓
init:1 ← FunnelHandler::registerFunnelItems()
Core actions/benchmarks/triggers are instantiated.
Pro IntegrationHandler also registers extra funnel
items on init:1.
↓
init:2 ← FunnelHandler::registerEarlyActiveTriggers()
Registers saved trigger listeners only when the
fluentcrm_funnel_arg_num_{name} filter already
exists. This catches events fired by other init:10
callbacks.
↓
init:10 ← FunnelHandler::handle()
Registers the automation runner callback.
↓
init:20 ← FunnelHandler::registerActiveTriggers()
Fallback listener pass for triggers whose arg-num
filter was not ready at init:2.
↓
init:1000 ← do_action('fluent_crm/after_init') from boot/app.php:44-46
TOO LATE for trigger/action/benchmark registration
↓
... runtime events fire ...
The single timing rule — instantiate every trigger / action / benchmark on fluentcrm_loaded priority < 10, or at the latest from an init:1 callback added before FluentCRM's init:2 early listener pass. fluent_crm/after_init is too late; init:10 can miss events fired by other init:10 callbacks.
File map
fluent-crm/
├── fluent-crm.php ← header + bootstrap entry
├── boot/
│ ├── app.php ← framework init, fluentcrm_loaded / addons_loaded dispatch
│ └── …
├── app/
│ ├── Api/
│ │ ├── config.php ← FluentCrmApi keys: contacts, tags, lists, extender, companies, event_tracker
│ │ ├── Classes/Contacts.php ← FluentCrmApi('contacts') backend
│ │ ├── Classes/Companies.php ← FluentCrmApi('companies') backend
│ │ ├── Classes/Tracker.php ← FluentCrmApi('event_tracker') backend
│ │ └── Classes/Extender.php ← smart-code extender API
│ ├── Functions/helpers.php ← FluentCrmApi(), fluentCrmDb(), fluentCrmTimestamp(), …
│ ├── Hooks/
│ │ ├── actions.php ← built-in `add_action` registrations
│ │ ├── filters.php
│ │ └── Handlers/ ← Funnel, Subscriber, Campaign, etc. handlers
│ ├── Http/Controllers/ ← REST endpoints (FunnelController, OptionsController, …)
│ ├── Models/ ← Subscriber, Company, EventTracker, Funnel, FunnelSequence, …
│ ├── Modules/MCP/ ← WP Abilities/MCP tools, guarded by wp_register_ability
│ ├── Services/
│ │ ├── Funnel/
│ │ │ ├── BaseTrigger.php
│ │ │ ├── BaseAction.php
│ │ │ ├── BaseBenchMark.php
│ │ │ ├── FunnelProcessor.php
│ │ │ └── FunnelHelper.php
│ │ ├── Helper.php
│ │ └── Libs/Parser/ ← smart code parser
│ └── Views/
└── ...
fluentcampaign-pro/
├── fluentcampaign-pro.php
└── app/
├── Hooks/Handlers/IntegrationHandler.php ← initFunnelActions / initBenchmarks / initTriggers at init:1
├── Models/Sequence*.php ← Pro email sequences
├── Modules/SMS/ ← Pro SMS module
├── Modules/MCP/ ← Pro Abilities/MCP registration
└── Services/Integrations/
├── TutorLms/CourseEnrollTrigger.php ← canonical trigger reference
├── TutorLms/AddToCourseAction.php ← canonical action reference
├── WooCommerce/ ← deep Woo integration
├── LearnDash/, LifterLms/, EDD/, AffiliateWP/, WishlistMember/
└── …
Models you will touch
| Model | Class | Notes |
|---|---|---|
| Subscriber | FluentCrm\App\Models\Subscriber | Core contact. getWpUserId(), tags, lists, email, status |
| Company | FluentCrm\App\Models\Company | Experimental CRM account/company model. Relation through fc_subscriber_pivot, plus Subscriber.company_id primary company |
| EventTracker | FluentCrm\App\Models\EventTracker | Experimental event timeline table fc_event_tracking; API key is event_tracker |
| Funnel | FluentCrm\App\Models\Funnel | An automation. status ∈ {published, draft, archived} |
| FunnelSequence | FluentCrm\App\Models\FunnelSequence | A step inside a funnel. action_name keys into your block. type ∈ {action, benchmark, conditional} |
| FunnelSubscriber | FluentCrm\App\Models\FunnelSubscriber | Join: contact <-> funnel. Tracks current sequence position. Main status uses values such as draft, pending, active, waiting, completed, cancelled, skipped |
| FunnelMetric | FluentCrm\App\Models\FunnelMetric | Per-step audit row. DB default / successful status is completed; handlers usually write only skipped or failed |
| Tag | FluentCrm\App\Models\Tag | Many-to-many with Subscriber via SubscriberPivot |
| Lists | FluentCrm\App\Models\Lists | Same |
| Webhook | FluentCrm\App\Models\Webhook | Inbound webhook receivers |
Status strings are column-specific: FunnelHelper::changeFunnelSubSequenceStatus() writes FunnelSubscriber.last_sequence_status and uses complete by default, while the main funnel-subscriber status and successful funnel metrics use completed. Do not normalize these into one word.
Global helpers
// API surface — preferred over direct Model writes
FluentCrmApi('contacts')->createOrUpdate($data);
FluentCrmApi('contacts')->getContact($emailOrId);
FluentCrmApi('companies')->createOrUpdate($companyData);
FluentCrmApi('event_tracker')->track($eventData);
// Database query builder — bypasses Eloquent for raw queries
fluentCrmDb()->table('fc_subscribers')->where('email', $email)->first();
// FunnelHelper — the single most-used utility from companion plugins
FunnelHelper::prepareUserData($wpUserId); // builds $subscriberData skeleton
FunnelHelper::getSubscriber($emailOrUserId); // resolves Subscriber model
FunnelHelper::ifAlreadyInFunnel($funnelId, $subId); // dedupe guard
FunnelHelper::removeSubscribersFromFunnel($funnelId, $subIds); // restart helper
FunnelHelper::getUpdateOptions(); // canonical "if exists" radio options
FunnelHelper::changeFunnelSubSequenceStatus($funnelSubId, $sequenceId, 'complete');
FunnelHelper::maybeExplodeFullName($subscriberData); // splits "John Doe" → first/last name
FunnelHelper::createWpUserFromSubscriber($subscriber, $sendWelcomeEmail);
FluentCrmApi, fluentCrmDb, FluentCrm (service container) all defined at helpers.php.
Contact lifecycle hooks
Every contact-state change fires both a legacy underscore-named action AND a new slash-named action (introduced in 2.8). Hook the slash version in new code; the underscore versions are deprecated but still fire for backwards compatibility:
| Modern hook | Legacy alias (deprecated) | Args | Source |
|---|---|---|---|
fluent_crm/contact_created | fluentcrm_contact_created | $subscriber | Subscriber.php:970-971, 1145-1146 |
fluent_crm/contact_updated | fluentcrm_contact_updated | $subscriber, $dirtyFields | Subscriber.php:1006-1007, 1148 |
fluent_crm/contact_email_changed | (no alias) | $subscriber, $oldEmail | Subscriber.php:1095 |
fluent_crm/contact_custom_data_updated | fluentcrm_contact_custom_data_updated | $newValues, $subscriber, $updateValues | Subscriber.php:669-670 |
Useful when you need to react to subscriber state independent of funnels — e.g. mirror contact data to an external CRM.
Smart-code extension
Custom template tags like {{my_plugin.foo}} register through the fluent_crm/extended_smart_codes filter (Helper.php:329, Extender.php:108). The Extender API:
FluentCrmApi('extender')->addSmartCode(
'my_plugin', // namespace key
__('My Plugin Tokens', 'my-plugin'), // group title
[
'my_plugin.contact_score' => 'Contact Score',
'my_plugin.last_purchase' => 'Last Purchase Date',
],
function ($code, $valueKey, $defaultValue, $subscriber) {
// Resolve $valueKey for $subscriber. Return string.
if ($valueKey === 'contact_score') {
return (string) get_user_meta($subscriber->user_id, 'my_score', true);
}
return $defaultValue;
}
);
Smart codes are evaluated at email render time AND inside dynamic field tokens for actions like "Update Custom Field". Fallback for unknown codes is apply_filters('fluentcrm_smartcode_fallback', $matches[0], $subscriber) — useful for legacy {{tag}} shapes.
Funnel layer architecture
┌──────────────────────────────────────────┐
│ Trigger fires (a real WP action) │
│ e.g. lw_lms_after_grant, tutor_after_ │
│ enrolled, woocommerce_new_order │
└────────────────────┬─────────────────────┘
│
┌────────────────────▼─────────────────────┐
│ FunnelHandler::mapTriggers │
│ (FunnelHandler.php:105-141) │
│ - looks up published funnels with │
│ matching trigger_name │
│ - dispatches fluentcrm_funnel_start_* │
│ - looks up published BENCHMARK │
│ sequences with matching action_name │
│ - dispatches fluentcrm_funnel_benchmark_│
│ start_* │
└────────┬──────────────────────┬──────────┘
│ │
┌──────────▼──────────┐ ┌────────▼──────────┐
│ BaseTrigger::handle │ │ BaseBenchMark:: │
│ → startFunnelSequ- │ │ handle │
│ ence (NEW run) │ │ → startFunnelFrom │
│ │ │ SequencePoint │
│ │ │ (RESUME) │
└──────────┬──────────┘ └────────┬──────────┘
│ │
┌────▼──────────────────────▼────┐
│ FunnelProcessor::processSequence│
│ (loops over sequence steps) │
│ - sequence type 'action' │
│ → fluentcrm_funnel_ │
│ sequence_handle_* │
│ → BaseAction::handle() │
│ - sequence type 'benchmark' │
│ → wait │
│ - sequence type 'conditional' │
│ → branch (Pro) │
└─────────────────────────────────┘
Decision matrix — pick the right contract
| You want to... | Extend |
|---|---|
| Start an automation when X happens | Trigger — fluentcrm-funnel-trigger |
| Do work for a contact at a sequence step | Action — fluentcrm-funnel-action |
| Pause the funnel until X happens, then resume | Benchmark — fluentcrm-funnel-benchmark |
| Add a multi-select picker field to any of the above | REST options filter — fluentcrm-rest-options |
Add {{my_code.foo}} template tags | Smart code extender — see "Smart-code extension" above |
| React to contact state without a funnel | Contact lifecycle hooks — see table above |
| Add a custom REST endpoint | WP REST API — FluentCRM has no special wrapper; use register_rest_route |
| Add a custom contact field | FluentCRM admin → Settings → Contact Custom Fields. No code path. |
Critical rules
- Always hook
fluentcrm_loadedpriority < 10 for trigger / action / benchmark registration. The single most-bitten timing trap. - Always seed
'settings'ingetBlock()for actions and benchmarks. One key pergetBlockFields()['fields']key. Without it the editor renders an empty panel and the JS console throwsTypeError: Cannot read properties of undefined. Triggers don't have this trap (defaults come from the abstractgetFunnelSettingsDefaults()/getFunnelConditionDefaults()methods). - Do not collapse
completeandcompleted.changeFunnelSubSequenceStatus()writeslast_sequence_status = complete; the main automation run andFunnelMetricsuccess state usecompleted. - Use
getWpUserId()not->user_idwhen reading the WP user from aSubscriberinstance. - Use
FluentCrmApi('contacts')->createOrUpdate()rather than directly instantiatingSubscribermodel in companion code. The API encapsulates list/tag attachment, double-optin handling, and lifecycle hook firing. - Pin Pro version detection on
2.8.0if you depend on the modern lifecycle. Older Pro releases predate stable BaseAction semantics. - Don't catch
fluentcrm_funnel_start_*directly. Always go through BaseTrigger; FluentCRM may add validation around that hook in future versions, and going around BaseTrigger means missingprepareEditorDetails(__force_run_actionsinjection, etc.). - Plugin-presence detection MUST use a file-load-time symbol.
class_exists('TopLevelClassFromMainFile')ordefined('CONST_NAME'). NEVERfunction_exists()for symbols declared inside another plugin'splugins_loadedcallback — non-deterministic load order makes the check pass on some installs and fail on others. See "Active-detection canon" above for the per-plugin canonical list.
Common mistakes
- Confusing the underscore vs slash hook variants — both fire today, but
fluentcrm_*is on the deprecation track. New integrations should hookfluent_crm/*. - Reaching for
class_exists('FluentCRM')to detect the plugin. There's no such class. Usedefined('FLUENTCRM'). - Bootstrap-instantiating triggers / actions on
plugins_loadedwithout thefluentcrm_loadedindirection. FluentCRM may not be loaded yet atplugins_loaded; even if it is, yourBaseTrigger::register()runs before FluentCRM's own boot, so the filters target an environment that isn't fully wired. - Calling internal model methods like
Subscriber::find()from outside FluentCRM. Stable, but the model behaviour changes between minor versions. PreferFluentCrmApi('contacts')->getContact()for reads. - Using
function_exists('helper_fn')to gate dep-aware registration. The classic load-order race — works on installs where the dep loads first, silently breaks on installs where it loads second. Symptom: trigger / action appears in the editor on some sites and not others, no error. Always use the file-scope class / constant canon listed in "Active-detection canon". - Pro-gating an action that doesn't actually need Pro.
BaseActionis in FluentCRM Free — many integration actions work without Pro. Only requireisFluentCRMProActive()when the action genuinely uses Pro-only APIs (advanced reporting, sequence email scheduling, conditional branching).
Cross-references
- Run
fluentcrm-funnel-triggerfor the trigger contract + the timing-trap deep dive. - Run
fluentcrm-funnel-actionfor the action contract + status-string canon. - Run
fluentcrm-funnel-benchmarkfor benchmark / goal-point contract. - Run
fluentcrm-rest-optionsfor therest_selectoroption filter pattern. - Run
fluentcrm-companies-modelfor company/account relations, custom company fields, and company hooks. - Run
fluentcrm-event-trackingforFluentCrmApi('event_tracker'), event timeline filters, and the Pro tracking-event automation trigger/action.
What this skill does NOT cover
- Email-campaign rendering / templates.
- The block-editor email designer.
- Webhook receiver / sender configuration.
- Detailed Companies or Event Tracking usage.
- Pro-only branching (conditional / A-B test) — separate contract.
- SMTP / mail driver integration.
- The CRM admin UI / Vue app.
References
- FluentCRM developer docs root
- Plugin entry —
fluent-crm.php - Bootstrap order —
boot/app.php:31-44,app/Hooks/actions.php,app/Hooks/Handlers/FunnelHandler.php - Model layer index —
app/Models/ - Funnel services —
app/Services/Funnel/ - Global helpers —
app/Functions/helpers.php - Pro integration handler —
fluentcampaign-pro/app/Hooks/Handlers/IntegrationHandler.php - Reference Pro trigger —
fluentcampaign-pro/app/Services/Integrations/TutorLms/CourseEnrollTrigger.php - Reference Pro action —
fluentcampaign-pro/app/Services/Integrations/TutorLms/AddToCourseAction.php - Verified source paths:
wp-content/plugins/fluent-crm/app/Models/Subscriber.phpwp-content/plugins/fluent-crm/app/Models/Company.phpwp-content/plugins/fluent-crm/app/Models/EventTracker.phpwp-content/plugins/fluent-crm/app/Services/Helper.phpwp-content/plugins/fluent-crm/app/Api/config.phpwp-content/plugins/fluent-crm/app/Api/Classes/Extender.phpwp-content/plugins/fluentcampaign-pro/fluentcampaign-pro.php
Gives 0 of the 12 instructions most project setup skills give in ~5.5k tokens
Counted across 999 of the 1,637 authors here whose files we hold, read 2026-08-06
- ask one question at a timein 29 of 999, across 28 files
- detect the package manager from lockfilesin 28 of 999, across 9 files
- present findings to the userin 25 of 999, across 4 files
- explore current repo statein 24 of 999, across 3 files
- update the agent skills block in place if it existsin 24 of 999, across 3 files
- install husky lint-staged and prettierin 23 of 999, across 4 files
- create the lintstagedrc filein 22 of 999, across 3 files
- commit all changed filesin 22 of 999, across 3 files
- run lint-staged to verify it worksin 22 of 999, across 3 files
- initialize huskyin 21 of 999, across 2 files
- create the husky pre-commit filein 21 of 999, across 2 files
- create a prettierrc file if missingin 21 of 999, across 2 files
Said here and by no other author read
- pick this skill before choosing an extension contract
- detect active plugins using file-scope constants or classes
- register triggers actions and benchmarks on fluentcrm_loaded
- use priority less than ten for fluentcrm_loaded registration
- use FluentCrmApi for contact and company operations
- use FunnelHelper for subscriber and funnel utilities
Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.