agentsclimarketplace

Fluentcrm overview

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

A community-maintained collection of agent skills for WordPress plugin and theme development.

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.

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

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 happensTriggerfluentcrm-funnel-trigger
Do work for a contact at a sequence stepActionfluentcrm-funnel-action
Pause the funnel until X happens, then resumeBenchmarkfluentcrm-funnel-benchmark
Add a multi-select picker field to any of the aboveREST options filterfluentcrm-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

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.

Keep looking

Skills are one crate of 328,083. 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.