Skip to content
View as Markdown

Contact Hooks

FluentCRM Core Intermediate

These action hooks fire during contact lifecycle events — creation, updates, tag/list changes, status transitions, notes, and more.

Contact Created & Updated

fluent_crm/contact_created

This action runs when a new contact is created.

Suppressed by silent imports

Bulk inserts skip this hook entirely when the FLUENTCRM_DISABLE_TAG_LIST_EVENTS constant is defined — that is what "import silently" does, so imported rows do not enroll into Contact Created automations. Do not rely on this hook to see every row that reaches fc_subscribers. The bulk hooks fluentcrm_contacts_imported_bulk and fluentcrm_contacts_updated_bulk are not suppressed — listen on those to see silently imported rows.

Deprecated alias

fluentcrm_contact_created fires alongside this hook with the same signature. It has been deprecated since 2.8.0 — use fluent_crm/contact_created.

Parameters

Usage:

php
add_action('fluent_crm/contact_created', function($subscriber) {
   // Do whatever you want with the newly created $subscriber
});

Source: app/Models/Subscriber.php


fluent_crm/contact_updated

This action runs when a contact is updated. It fires from many paths — the model's bulk import and createOrUpdate(), the REST controller, the auto-subscribe handler, the manage-subscription preference form, and the Pro Update Contact Property automation action.

Deprecated alias

fluentcrm_contact_updated fires alongside this hook with the same signature on every path except the auto-subscribe handler, which fires only the fluent_crm/ name. It has been deprecated since 2.8.0 — use fluent_crm/contact_updated.

Parameters

Usage:

php
add_action('fluent_crm/contact_updated', function($subscriber, $dirtyFields) {
   // $dirtyFields contains the changed field values
}, 10, 2);

Source: app/Models/Subscriber.php, app/Http/Controllers/SubscriberController.php, app/Hooks/Handlers/AutoSubscribeHandler.php, app/Hooks/Handlers/PrefFormHandler.php, fluentcampaign-pro/app/Services/Funnel/Actions/UpdateContactPropertyAction.php


fluent_crm/contact_updated_with_changes

This action provides detailed change tracking, including both old and new values. Fires when a contact is updated via the admin UI or Fluent Forms.

Parameters

  • $subscriber Subscriber Model
  • $dirtyFields Array - changed fields (or custom field values)
  • $oldData Mixed - original data before changes (Subscriber Model or old custom fields array)
  • $meta Array - context info e.g. ['source' => 'web', 'type' => 'all_fields']

Usage:

php
add_action('fluent_crm/contact_updated_with_changes', function($subscriber, $dirtyFields, $oldData, $meta) {
   if ($meta['type'] === 'custom_fields_only') {
       // Only custom fields were changed
   }
   // Compare $dirtyFields with $oldData for detailed change tracking
}, 10, 4);

Source: app/Http/Controllers/SubscriberController.php, app/Services/ExternalIntegrations/FluentForm/Bootstrap.php


fluent_crm/contact_custom_data_updated

This action runs when a contact's custom field values are updated.

Parameters

  • $newValues Array - new custom field values
  • $subscriber Subscriber Model
  • $updateValues Array - the values that were actually updated

Usage:

php
add_action('fluent_crm/contact_custom_data_updated', function($newValues, $subscriber, $updateValues) {
   // React to custom field changes
}, 10, 3);

Source: app/Models/Subscriber.php


fluent_crm/contact_email_changed

This action hook fires when a subscriber's email has been changed to a new email address.

Parameters

Usage:

php
add_action('fluent_crm/contact_email_changed', function($subscriber, $oldEmail) {
   // the contact's email changed. You can run your code here
}, 10, 2);

Source: app/Models/Subscriber.php, app/Http/Controllers/SubscriberController.php, app/Hooks/Handlers/AutoSubscribeHandler.php, app/Hooks/Handlers/ExternalPages.php, app/Modules/MCP/Tools/ContactTools.php


fluent_crm/subscriber_avatar_update

This action fires when a contact's avatar is updated via the admin profile editor.

Parameters

Usage:

php
add_action('fluent_crm/subscriber_avatar_update', function($subscriber, $oldValue) {
   // Avatar was changed
}, 10, 2);

Source: app/Http/Controllers/SubscriberController.php


Tags & Lists Assignment

Suppressed by silent imports

Like fluent_crm/contact_created, all four tag/list hooks are skipped when the FLUENTCRM_DISABLE_TAG_LIST_EVENTS constant is defined — the CSV importer and the WP-user importer both define it. They also only fire for rows that actually changed, so re-attaching an existing tag or list is silent.

fluent_crm/contact_added_to_tags

This action runs when tags have been added to a contact.

Note: The legacy hook fluentcrm_contact_added_to_tags also fires with reversed parameter order: ($tagIds, $subscriber).

Parameters

Usage:

php
add_action('fluent_crm/contact_added_to_tags', function($subscriber, $tagIds) {
   // Do whatever you want here
}, 10, 2);

Source: app/Models/Subscriber.php


fluent_crm/contact_added_to_lists

This action runs when lists have been added to a contact.

Note: The legacy hook fluentcrm_contact_added_to_lists also fires with reversed parameter order: ($listIds, $subscriber).

Parameters

Usage:

php
add_action('fluent_crm/contact_added_to_lists', function($subscriber, $listIds) {
   // Do whatever you want here
}, 10, 2);

Source: app/Models/Subscriber.php


fluent_crm/contact_removed_from_tags

This action runs when tags have been removed from a contact.

Note: The legacy hook fluentcrm_contact_removed_from_tags also fires with reversed parameter order: ($tagIds, $subscriber).

Parameters

Usage:

php
add_action('fluent_crm/contact_removed_from_tags', function($subscriber, $tagIds) {
   // Do whatever you want here
}, 10, 2);

Source: app/Models/Subscriber.php


fluent_crm/contact_removed_from_lists

This action runs when lists have been removed from a contact.

Note: The legacy hook fluentcrm_contact_removed_from_lists also fires with reversed parameter order: ($listIds, $subscriber).

Parameters

Usage:

php
add_action('fluent_crm/contact_removed_from_lists', function($subscriber, $listIds) {
   // Do whatever you want here
}, 10, 2);

Source: app/Models/Subscriber.php


Status Changes

fluent_crm/subscriber_status_changed

This action fires whenever a subscriber's status changes, providing both old and new status values. This is the general status change hook — the dynamic hook below also fires alongside it.

Parameters

  • $subscriber Subscriber Model
  • $oldStatus string - previous status
  • $newStatus string - new status

Older Pro versions pass only two arguments

FluentCampaign Pro 3.1.10 and earlier fired this hook from the Change Contact Status automation action without $newStatus, so a callback declared with three required parameters threw an ArgumentCountError when that action ran. If you support those versions, give the third parameter a default and fall back to the model:

php
add_action('fluent_crm/subscriber_status_changed', function($subscriber, $oldStatus, $newStatus = null) {
   $newStatus = $newStatus ?: $subscriber->status;
}, 10, 3);

Usage:

php
add_action('fluent_crm/subscriber_status_changed', function($subscriber, $oldStatus, $newStatus) {
   // React to any status change
}, 10, 3);

Source: app/Models/Subscriber.php, app/Http/Controllers/SubscriberController.php, fluentcampaign-pro/app/Services/Funnel/Actions/ChangeContactStatusAction.php


fluentcrm_subscriber_status_to_{$new_status}

This dynamic action hook fires when a subscriber's status has been changed to a specific new status.

Possible Hooks

One per status returned by fluent_crm/contact_statuses:

  • fluentcrm_subscriber_status_to_subscribed
  • fluentcrm_subscriber_status_to_pending
  • fluentcrm_subscriber_status_to_unsubscribed
  • fluentcrm_subscriber_status_to_transactional
  • fluentcrm_subscriber_status_to_bounced
  • fluentcrm_subscriber_status_to_complained
  • fluentcrm_subscriber_status_to_spammed

Parameters

  • $subscriber Subscriber Model - already saved with the new status
  • $oldStatus string - old status of the contact

TIP

FluentCRM itself listens on several of these — subscribed resumes paused automations, while unsubscribed, bounced, complained and spammed run the unsubscribe cleanup. Your callback runs alongside those, not instead of them.

Usage:

php
add_action('fluentcrm_subscriber_status_to_subscribed', function($subscriber, $oldStatus) {
   // the subscriber got subscribed status. You can run your code here
}, 10, 2);

Source: app/Models/Subscriber.php, fluentcampaign-pro/app/Services/Funnel/Actions/ChangeContactStatusAction.php


fluent_crm/subscriber_unsubscribed_from_web_ui

This action hook fires when a subscriber unsubscribes from the web UI. Please note that fluentcrm_subscriber_status_to_unsubscribed also fires before this action.

Parameters

  • $subscriber Subscriber Model
  • $postedData array - post data of the unsubscribe form as key value pair

Usage:

php
add_action('fluent_crm/subscriber_unsubscribed_from_web_ui', function($subscriber, $data) {
   // the contact unsubscribed from web UI. Do your stuff here
}, 10, 2);

Source: app/Hooks/Handlers/ExternalPages.php


fluent_crm/subscriber_confirmed_via_double_optin

This action hook fires when a subscriber confirms via double optin by clicking the DOI link. Please note that fluentcrm_subscriber_status_to_subscribed also fires before this action.

Parameters

Usage:

php
add_action('fluent_crm/subscriber_confirmed_via_double_optin', function($subscriber) {
   // the contact confirmed the subscription via double optin
});

Source: app/Hooks/Handlers/ExternalPages.php


fluent_crm/subscriber_sms_status_changed

This action fires when a subscriber's SMS status is changed.

Parameters

  • $subscriber Subscriber Model
  • $oldStatus string - previous SMS status
  • $newStatus string - new SMS status

Usage:

php
add_action('fluent_crm/subscriber_sms_status_changed', function($subscriber, $oldStatus, $newStatus) {
   // SMS status changed
}, 10, 3);

Source: app/Http/Controllers/SubscriberController.php


Contact Type Changes

fluent_crm/subscriber_contact_type_to_{$new_type}

This action hook fires when a subscriber's contact_type has been changed to a new type.

Only fires from the bulk action

This is dispatched from the Change Contact Type bulk action in the contacts list, once per contact whose type actually changed. Editing a single contact's type on the profile screen does not fire it.

Possible Hooks

One per type returned by fluent_crm/contact_types:

  • fluent_crm/subscriber_contact_type_to_lead
  • fluent_crm/subscriber_contact_type_to_customer

Parameters

  • $subscriber Subscriber Model - already saved with the new type
  • $oldType string - old type of the contact (eg: lead | customer)

Usage:

php
add_action('fluent_crm/subscriber_contact_type_to_customer', function($subscriber, $oldType) {
   // the contact's type changed to customer. You can run your code here
}, 10, 2);

Source: app/Http/Controllers/SubscriberController.php


Unsubscribe from Email

fluent_crm/before_contact_unsubscribe_from_email

This action runs just after a contact unsubscribes by clicking the unsubscribe link in an email or from the email header.

Parameters

Usage:

php
add_action('fluent_crm/before_contact_unsubscribe_from_email', function($subscriber, $campaignEmail, $scope) {
   // Do your stuff here
}, 10, 3);

Example:

Unsubscribe a user from specific lists instead of globally, depending on the sending lists:

php
add_action('fluent_crm/before_contact_unsubscribe_from_email', function($subscriber, $campaignEmail, $scope) {
   if(!$campaignEmail || !$campaignEmail->campaign) {
        return false;
   }

   $settings = $campaignEmail->campaign->settings;
   $sendingType = \FluentCrm\Framework\Support\Arr::get($settings, 'sending_filter');

   if($sendingType != 'list_tag') {
        return false;
   }

   $sendingListIds = [];
   foreach ($settings['subscribers'] as $segment) {
        $sendingListIds[] = \FluentCrm\Framework\Support\Arr::get($segment, 'list', 0);
   }

   $sendingListIds = array_values(array_filter(array_unique($sendingListIds)));
   $sendingListIds = array_map('intval', $sendingListIds);
   if(empty($sendingListIds)) {
        return false;
   }

   $subscriber->detachLists($sendingListIds);

   wp_send_json_success([
        'message'      => 'You are unsubscribed from the lists',
        'redirect_url' => ''
   ], 200);
}, 10, 3);

Source: app/Hooks/Handlers/ExternalPages.php


Contact Notes

fluent_crm/note_added

This action fires when a note is added to a contact.

Parameters

Usage:

php
add_action('fluent_crm/note_added', function($subscriberNote, $subscriber, $note) {
   // A note was added to the contact
}, 10, 3);

Source: app/Http/Controllers/SubscriberController.php


fluent_crm/note_updated

This action fires when a contact note is updated.

Parameters

Usage:

php
add_action('fluent_crm/note_updated', function($subscriberNote, $subscriber, $note) {
   // A contact note was updated
}, 10, 3);

Source: app/Http/Controllers/SubscriberController.php


fluent_crm/note_delete

This action fires after a contact note is deleted from the REST API, both for a single delete and once per note for the bulk delete.

WARNING

The note row is already gone when this fires, and only its ID is passed. Capture what you need on fluent_crm/note_added or fluent_crm/note_updated.

Parameters

Usage:

php
add_action('fluent_crm/note_delete', function($noteId, $subscriber) {
   // A contact note was deleted
}, 10, 2);

Source: app/Http/Controllers/SubscriberController.php


fluent_crm/note_deleted

The MCP counterpart of fluent_crm/note_delete, fired when a note is deleted through the MCP contact tools rather than the admin REST API.

Different name, different signature

Note the past-tense name and that the second argument is a contact ID, not a model. Hook both fluent_crm/note_delete and fluent_crm/note_deleted if you need to cover every deletion path.

Parameters

  • $deletedId INT - Note ID
  • $subscriberId INT - Contact ID the note belonged to

Usage:

php
add_action('fluent_crm/note_deleted', function($deletedId, $subscriberId) {
   // A contact note was deleted via MCP
}, 10, 2);

Source: app/Modules/MCP/Tools/ContactTools.php


Birthday

fluentcrm_contact_birthday

Pro

Fires when a contact's birthday occurs. Processed in batch during scheduled birthday checks. Used for triggering birthday automations.

Parameters

Usage:

php
add_action('fluentcrm_contact_birthday', function($subscriber) {
    // Send birthday greeting, apply tags, etc.
});

Source: fluentcampaign-pro/app/Hooks/Handlers/IntegrationHandler.php


Bulk Import

fluentcrm_contacts_imported_bulk

This action fires once at the end of every bulk import run with all the contacts the run inserted. The CSV importer, the WP-user importer, and the integration importers all funnel through Subscriber::import(), so they all fire it.

Fires even for silent imports

Unlike fluent_crm/contact_created, this hook is not suppressed by the FLUENTCRM_DISABLE_TAG_LIST_EVENTS constant. When an admin imports silently, this hook and fluentcrm_contacts_updated_bulk are the only signals that the rows arrived.

Parameters

  • $insertedModels Array - the newly inserted Subscriber Models. An empty array when the run only matched existing contacts.

Usage:

php
add_action('fluentcrm_contacts_imported_bulk', function($insertedModels) {
   foreach ($insertedModels as $subscriber) {
       // Sync each newly imported contact to your system
   }
});

Source: app/Models/Subscriber.php


fluentcrm_contacts_updated_bulk

This action fires immediately after fluentcrm_contacts_imported_bulk, with the already-existing contacts the import run matched by email.

Includes unchanged contacts

Every matched existing contact is included, even when its incoming row changed nothing — unlike the per-row fluent_crm/contact_updated, which only fires for rows with actual changes. The collection is empty when the import ran with "update existing contacts" disabled.

Parameters

  • $updatedModels Collection - Subscriber Models of the existing contacts matched by the import

Usage:

php
add_action('fluentcrm_contacts_updated_bulk', function($updatedModels) {
   foreach ($updatedModels as $subscriber) {
       // React to re-imported contacts
   }
});

Source: app/Models/Subscriber.php


Bulk Deletion

fluentcrm_before_subscribers_deleted

This action fires before contacts are deleted in bulk.

Parameters

  • $contactIds Array - IDs of contacts about to be deleted

Usage:

php
add_action('fluentcrm_before_subscribers_deleted', function($contactIds) {
   // Clean up related data before contacts are deleted
});

Source: app/Services/Helper.php


fluentcrm_after_subscribers_deleted

This action fires after contacts have been deleted in bulk.

Parameters

  • $contactIds Array - IDs of contacts that were deleted

Usage:

php
add_action('fluentcrm_after_subscribers_deleted', function($contactIds) {
   // Post-deletion cleanup
});

Source: app/Services/Helper.php


Advanced Filter Providers

fluentcrm_contacts_filter_{$provider}

This dynamic action is the extension point behind the contacts Advanced Filter UI — and everything built on it: saved segments, campaign recipient selection, and the Pro automation conditions. For every filter group, the ContactsQuery service fires one action per provider via do_action_ref_array(), handing the listener the query builder so it can add its WHERE constraints in place. The return value is ignored — the query object itself is the contract.

Possible Hooks

Core registers subscriber, segment, custom_fields, activities, and event_tracking (its listener is registered even while the experimental Event Tracking feature is off — only the Advanced Filter UI hides it). Integrations register their own providers: woo, edd, learndash, lifterlms, tutorlms, and aff_wp come from FluentCampaign Pro, and fluent_cart is registered by the Fluent Cart plugin.

Parameters

  • $query Query Builder - the nested where-group for the current filter group. Add constraints to it in place; do not execute it.
  • $filterItems Array - the filter rows configured for this provider, each with property, operator, and value keys

Unhandled providers fail closed

If no listener is registered for a provider name (has_action() returns false), ContactsQuery adds whereRaw('1 = 0') so the whole filter group matches nothing. That is deliberate: a provider that loses its handler (Pro deactivated, integration disabled) must not silently widen a campaign audience to "everyone". It also means your custom provider's listener must be registered on every request before the query runs — otherwise every segment or campaign using it quietly resolves to zero contacts.

No & in your callback

Although the hook is fired with do_action_ref_array(..., [&$q, $items]), the query builder is an object, so a plain function ($query, $filterItems) signature mutates it just fine — this is exactly how every core and Pro listener is declared. Do not add & to the parameter.

Usage:

php
add_action('fluentcrm_contacts_filter_my_plugin', function ($query, $filterItems) {
    foreach ($filterItems as $filterItem) {
        if ($filterItem['property'] == 'vip_level') {
            $query->where('fc_subscribers.total_points', '>=', (int) $filterItem['value']);
        }
    }
}, 10, 2);

To surface your provider in the Advanced Filter UI, also register its fields through the fluentcrm_advanced_filter_options filter — the provider key there becomes the {provider} part of this hook name.

Source: app/Services/ContactsQuery.php