Skip to content
Back to skills

Fluentcrm Overview

ASecurity

Orient skill for FluentCRM extension development. Covers the

  • 22 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added June 5, 2026
developmentgophpreactvuenodeapidatabasebackend

Works with

  • api

Security analysis

A100/100

Scanned June 5, 2026

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

Installs into .claude/skills of the current project.

Are you the author of Fluentcrm Overview?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Fluentcrm Overview
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/lonsdale201-fluentcrm-overview/badge)](https://www.skillsdirectory.com/skills/lonsdale201-fluentcrm-overview)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: fluentcrm-overview
description: 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 → fluent_crm/after_init), the model layer
  (Subscriber, 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.
author: Soczó Kristóf
contact: mailto:lonsdale201@hotmail.com
plugin: fluent-crm
plugin-version-tested: "2.9.87"
api-stable-since: "2.7"
php-min: "7.4"
last-updated: "2026-05-10"
docs:
  - https://developers.fluentcrm.com/
source-refs:
  - fluent-crm.php
  - boot/app.php
  - app/Functions/helpers.php
  - app/Models/Subscriber.php
  - app/Services/Helper.php
  - app/Api/Classes/Extender.php
---

# 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 | `2.9.87` | [fluent-crm.php:6](fluent-crm.php) |
| Pro slug | `fluentcampaign-pro` | header |
| Pro version | `2.9.86` | [fluentcampaign-pro.php](fluentcampaign-pro.php) |
| Boot constant | `FLUENTCRM` ('fluentcrm') | [fluent-crm.php:20](fluent-crm.php) |
| Pro constant | `FLUENTCAMPAIGN_PLUGIN_VERSION` | [fluentcampaign-pro.php](fluentcampaign-pro.php) |
| Path constant (Free) | `FLUENTCRM_PLUGIN_PATH` | [fluent-crm.php:22](fluent-crm.php) |
| Path constant (Pro) | `FLUENTCAMPAIGN_PLUGIN_PATH` | [fluentcampaign-pro.php:20](fluentcampaign-pro.php) |
| Min PHP (verified) | 7.4 | composer.json |

**Active-detection** — never test for `class_exists('FluentCRM')` (no such class). Use:

```php
public static function isFluentCRMActive(): bool {
    return defined('FLUENTCRM');
}

public static function isFluentCRMProActive(): bool {
    return defined('FLUENTCAMPAIGN_PLUGIN_VERSION')
        && version_compare(FLUENTCAMPAIGN_PLUGIN_VERSION, '2.8.0', '>=');
}
```

(The `2.8.0` floor is the BaseTrigger / BaseAction lifecycle stability point. Drop it if your code only uses model APIs.)

### 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` requires `fluentcampaign_boot.php` at file scope, which `define()`s the constant |
| 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):

```php
// 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 | Yes |
| 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
                              FunnelHandler::handle runs (actions.php:76)
                                 → reads fluentcrm_funnel_settings option
                                 → for each saved trigger name:
                                     $argNum = apply_filters('fluentcrm_funnel_arg_num_'.$name, 1)
                                     add_action($name, ..., 10, $argNum)   ← LOCKED IN
        ↓
init:1000                   ← do_action('fluent_crm/after_init') from boot/app.php:44-46
                              TOO LATE for argNum filters
        ↓
... runtime events fire ...
```

**The single timing rule** — register every trigger / action / benchmark on `fluentcrm_loaded` priority < 10. Hooking on `fluent_crm/after_init` or `init` causes silent multi-arg drops. See `fluentcrm-funnel-trigger` for the explanation.

## File map

```
fluent-crm/
├── fluent-crm.php                   ← header + bootstrap entry
├── boot/
│   ├── app.php                      ← framework init, fluentcrm_loaded / addons_loaded dispatch
│   └── …
├── app/
│   ├── Api/
│   │   ├── Classes/Extender.php     ← public API surface (smart code extender, contact API)
│   │   └── Classes/Contact.php      ← fluentCrmApi('contacts') backend
│   ├── 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, Funnel, FunnelSequence, …
│   ├── 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
    └── 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` |
| 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 |
| FunnelMetric | `FluentCrm\App\Models\FunnelMetric` | Per-step audit row. `status` ∈ `{pending, complete, skipped, 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 |

The status string canon is **`'complete'`, NOT `'completed'`** — see `fluentcrm-funnel-action` for the gotcha.

## Global helpers

```php
// API surface — preferred over direct Model writes
FluentCrmApi('contacts')->createOrUpdate($data);
FluentCrmApi('contacts')->getContact($emailOrId);

// 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](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](Subscriber.php) |
| `fluent_crm/contact_updated` | `fluentcrm_contact_updated` | `$subscriber, $dirtyFields` | [Subscriber.php:1006-1007, 1148](Subscriber.php) |
| `fluent_crm/contact_email_changed` | (no alias) | `$subscriber, $oldEmail` | [Subscriber.php:1095](Subscriber.php) |
| `fluent_crm/contact_custom_data_updated` | `fluentcrm_contact_custom_data_updated` | `$newValues, $subscriber, $updateValues` | [Subscriber.php:669-670](Subscriber.php) |

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](Helper.php), [Extender.php:108](Extender.php)). The `Extender` API:

```php
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_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).
- **Status string canon is `'complete'`** (not `'completed'`). Applies to every `changeFunnelSubSequenceStatus` and `$funnelMetric->status`.
- **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.

## What this skill does NOT cover

- Email-campaign rendering / templates.
- The block-editor email designer.
- Webhook receiver / sender configuration.
- Pro-only branching (conditional / A-B test) — separate contract.
- SMTP / mail driver integration.
- The CRM admin UI / Vue app.

## References

- [FluentCRM developer docs root](https://developers.fluentcrm.com/)
- Plugin entry — `fluent-crm.php`
- Bootstrap order — `boot/app.php:41-46`
- 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`

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.