agentsclimarketplace

Fluentcrm overview

Skill Lonsdale201/wp-agent-skills/fluentcrm/fluentcrm-overview

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.From its SKILL.md

Install
npx -y skills add Lonsdale201/wp-agent-skills --skill fluentcrm-overview

Assembled 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.

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

FieldValueSource
Slugfluent-crmheader
Version3.1.8fluent-crm.php
Pro slugfluentcampaign-proheader
Pro version3.1.8fluentcampaign-pro.php
Boot constantFLUENTCRM ('fluentcrm')fluent-crm.php:20
Pro constantFLUENTCAMPAIGN_PLUGIN_VERSIONfluentcampaign-pro.php
Path constant (Free)FLUENTCRM_PLUGIN_PATHfluent-crm.php:22
Path constant (Pro)FLUENTCAMPAIGN_PLUGIN_PATHfluentcampaign-pro.php:20
Min PHP (verified)7.4composer.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):

PluginUseWhy
WooCommerceclass_exists('WooCommerce')woocommerce.php includes class-woocommerce.php at file scope
FluentCampaign Prodefined('FLUENTCAMPAIGN_PLUGIN_VERSION')fluentcampaign-pro.php defines the constant before bootstrapping
WC Membershipsclass_exists('WC_Memberships_Loader')top-level loader class, WC_Memberships itself is loaded inside init_plugin() and is racy
LW LMSclass_exists('LightweightPlugins\\LMS\\Plugin')autoloader registered at file scope
Jet Engineclass_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

CapabilityFree (FluentCRM)Pro (FluentCampaign)
Contacts, lists, tags, segmentsYesYes
Email campaigns, sequences, templatesYesYes
Forms, opt-insYesYes
Funnel triggers (BaseTrigger)Yes — register from any pluginYes — many built-in (Tutor, LearnDash, LifterLMS, EDD, WishlistMember, AffiliateWP, Woo Subscriptions)
Funnel actions (BaseAction)Yes — register from any pluginYes — many built-in (advanced Woo, sequence add/remove, custom field updates)
Funnel benchmarks (BaseBenchMark)Yes — register from any pluginYes — additional built-ins
Conditional / A-B branching nodeNoYes
Companies moduleYes, experimental flag company_moduleYes
Event trackingYes, experimental flag event_trackingAdds trigger/action/conditions
MCP / Abilities API surfaceYes, guarded by wp_register_ability + mcp_enabledAdds Pro abilities
Webhook receiver / senderYesYes
SMTP / mail driver layerFree uses WP mail / Fluent SMTPPro 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

ModelClassNotes
SubscriberFluentCrm\App\Models\SubscriberCore contact. getWpUserId(), tags, lists, email, status
CompanyFluentCrm\App\Models\CompanyExperimental CRM account/company model. Relation through fc_subscriber_pivot, plus Subscriber.company_id primary company
EventTrackerFluentCrm\App\Models\EventTrackerExperimental event timeline table fc_event_tracking; API key is event_tracker
FunnelFluentCrm\App\Models\FunnelAn automation. status ∈ {published, draft, archived}
FunnelSequenceFluentCrm\App\Models\FunnelSequenceA step inside a funnel. action_name keys into your block. type ∈ {action, benchmark, conditional}
FunnelSubscriberFluentCrm\App\Models\FunnelSubscriberJoin: contact <-> funnel. Tracks current sequence position. Main status uses values such as draft, pending, active, waiting, completed, cancelled, skipped
FunnelMetricFluentCrm\App\Models\FunnelMetricPer-step audit row. DB default / successful status is completed; handlers usually write only skipped or failed
TagFluentCrm\App\Models\TagMany-to-many with Subscriber via SubscriberPivot
ListsFluentCrm\App\Models\ListsSame
WebhookFluentCrm\App\Models\WebhookInbound 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 hookLegacy alias (deprecated)ArgsSource
fluent_crm/contact_createdfluentcrm_contact_created$subscriberSubscriber.php:970-971, 1145-1146
fluent_crm/contact_updatedfluentcrm_contact_updated$subscriber, $dirtyFieldsSubscriber.php:1006-1007, 1148
fluent_crm/contact_email_changed(no alias)$subscriber, $oldEmailSubscriber.php:1095
fluent_crm/contact_custom_data_updatedfluentcrm_contact_custom_data_updated$newValues, $subscriber, $updateValuesSubscriber.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 happensTrigger — fluentcrm-funnel-trigger
Do work for a contact at a sequence stepAction — fluentcrm-funnel-action
Pause the funnel until X happens, then resumeBenchmark — fluentcrm-funnel-benchmark
Add a multi-select picker field to any of the aboveREST options filter — fluentcrm-rest-options
Add {{my_code.foo}} template tagsSmart code extender — see "Smart-code extension" above
React to contact state without a funnelContact lifecycle hooks — see table above
Add a custom REST endpointWP REST API — FluentCRM has no special wrapper; use register_rest_route
Add a custom contact fieldFluentCRM admin → Settings → Contact Custom Fields. No code path.

Critical rules

  • Always hook fluentcrm_loaded priority < 10 for trigger / action / benchmark registration. The single most-bitten timing trap.
  • Always seed 'settings' in getBlock() for actions and benchmarks. One key per getBlockFields()['fields'] key. Without it the editor renders an empty panel and the JS console throws TypeError: Cannot read properties of undefined. Triggers don't have this trap (defaults come from the abstract getFunnelSettingsDefaults() / getFunnelConditionDefaults() methods).
  • Do not collapse complete and completed. changeFunnelSubSequenceStatus() writes last_sequence_status = complete; the main automation run and FunnelMetric success state use completed.
  • Use getWpUserId() not ->user_id when reading the WP user from a Subscriber instance.
  • Use FluentCrmApi('contacts')->createOrUpdate() rather than directly instantiating Subscriber model in companion code. The API encapsulates list/tag attachment, double-optin handling, and lifecycle hook firing.
  • Pin Pro version detection on 2.8.0 if 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 missing prepareEditorDetails (__force_run_actions injection, etc.).
  • Plugin-presence detection MUST use a file-load-time symbol. class_exists('TopLevelClassFromMainFile') or defined('CONST_NAME'). NEVER function_exists() for symbols declared inside another plugin's plugins_loaded callback — 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 hook fluent_crm/*.
  • Reaching for class_exists('FluentCRM') to detect the plugin. There's no such class. Use defined('FLUENTCRM').
  • Bootstrap-instantiating triggers / actions on plugins_loaded without the fluentcrm_loaded indirection. FluentCRM may not be loaded yet at plugins_loaded; even if it is, your BaseTrigger::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. Prefer FluentCrmApi('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. BaseAction is in FluentCRM Free — many integration actions work without Pro. Only require isFluentCRMProActive() when the action genuinely uses Pro-only APIs (advanced reporting, sequence email scheduling, conditional branching).

Cross-references

  • Run fluentcrm-funnel-trigger for the trigger contract + the timing-trap deep dive.
  • Run fluentcrm-funnel-action for the action contract + status-string canon.
  • Run fluentcrm-funnel-benchmark for benchmark / goal-point contract.
  • Run fluentcrm-rest-options for the rest_selector option filter pattern.
  • Run fluentcrm-companies-model for company/account relations, custom company fields, and company hooks.
  • Run fluentcrm-event-tracking for FluentCrmApi('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.php
    • wp-content/plugins/fluent-crm/app/Models/Company.php
    • wp-content/plugins/fluent-crm/app/Models/EventTracker.php
    • wp-content/plugins/fluent-crm/app/Services/Helper.php
    • wp-content/plugins/fluent-crm/app/Api/config.php
    • wp-content/plugins/fluent-crm/app/Api/Classes/Extender.php
    • wp-content/plugins/fluentcampaign-pro/fluentcampaign-pro.php

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.