Contact Hooks
FluentCRM Core IntermediateThese 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
$subscriberSubscriber Model
Usage:
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
$subscriberSubscriber Model - already saved$dirtyFieldsArray - the changed fields, keyed by column name, holding the new values. Usefluent_crm/contact_updated_with_changesif you also need the old values.
Usage:
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
$subscriberSubscriber Model$dirtyFieldsArray - changed fields (or custom field values)$oldDataMixed - original data before changes (Subscriber Model or old custom fields array)$metaArray - context info e.g.['source' => 'web', 'type' => 'all_fields']
Usage:
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
$newValuesArray - new custom field values$subscriberSubscriber Model$updateValuesArray - the values that were actually updated
Usage:
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
$subscriberSubscriber Model$oldEmailstring - Old Email Address
Usage:
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
$subscriberSubscriber Model$oldValuestring - previous avatar URL
Usage:
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_tagsalso fires with reversed parameter order:($tagIds, $subscriber).
Parameters
$subscriberSubscriber Model$attachedTagIdsArray - tag IDs that were added
Usage:
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_listsalso fires with reversed parameter order:($listIds, $subscriber).
Parameters
$subscriberSubscriber Model$attachedListIdsArray - list IDs that were added
Usage:
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_tagsalso fires with reversed parameter order:($tagIds, $subscriber).
Parameters
$subscriberSubscriber Model$detachedTagIdsArray - tag IDs that were removed
Usage:
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_listsalso fires with reversed parameter order:($listIds, $subscriber).
Parameters
$subscriberSubscriber Model$detachedListIdsArray - list IDs that were removed
Usage:
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
$subscriberSubscriber Model$oldStatusstring - previous status$newStatusstring - 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:
add_action('fluent_crm/subscriber_status_changed', function($subscriber, $oldStatus, $newStatus = null) {
$newStatus = $newStatus ?: $subscriber->status;
}, 10, 3);Usage:
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_subscribedfluentcrm_subscriber_status_to_pendingfluentcrm_subscriber_status_to_unsubscribedfluentcrm_subscriber_status_to_transactionalfluentcrm_subscriber_status_to_bouncedfluentcrm_subscriber_status_to_complainedfluentcrm_subscriber_status_to_spammed
Parameters
$subscriberSubscriber Model - already saved with the new status$oldStatusstring - 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:
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
$subscriberSubscriber Model$postedDataarray - post data of the unsubscribe form as key value pair
Usage:
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
$subscriberSubscriber Model
Usage:
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
$subscriberSubscriber Model$oldStatusstring - previous SMS status$newStatusstring - new SMS status
Usage:
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_leadfluent_crm/subscriber_contact_type_to_customer
Parameters
$subscriberSubscriber Model - already saved with the new type$oldTypestring - old type of the contact (eg: lead | customer)
Usage:
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
$subscriberSubscriber Model$campaignEmailCampaignEmail Model or null$scopestring -'from_header'or'web_ui'
Usage:
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:
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
$subscriberNoteSubscriberNote Model$subscriberSubscriber Model$noteArray - note data
Usage:
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
$subscriberNoteSubscriberNote Model$subscriberSubscriber Model$noteArray - updated note data
Usage:
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
$noteIdINT - Note ID$subscriberSubscriber Model
Usage:
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
$deletedIdINT - Note ID$subscriberIdINT - Contact ID the note belonged to
Usage:
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
ProFires when a contact's birthday occurs. Processed in batch during scheduled birthday checks. Used for triggering birthday automations.
Parameters
$subscriberSubscriber Model
Usage:
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
$insertedModelsArray - the newly inserted Subscriber Models. An empty array when the run only matched existing contacts.
Usage:
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
$updatedModelsCollection - Subscriber Models of the existing contacts matched by the import
Usage:
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
$contactIdsArray - IDs of contacts about to be deleted
Usage:
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
$contactIdsArray - IDs of contacts that were deleted
Usage:
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
$queryQuery Builder - the nested where-group for the current filter group. Add constraints to it in place; do not execute it.$filterItemsArray - the filter rows configured for this provider, each withproperty,operator, andvaluekeys
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:
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