Extending PrimoCRM
Everything extendable in PrimoCRM is collected through a WordPress filter, so your add-on registers into it with no core edits. These are the patterns first-party Pro features use too. See the full list in Actions and Filters.
React to an event
Section titled “React to an event”The simplest extension is reacting to something that happened. Use the action hooks.
add_action( 'primocrm_contact_tag_added', function ( $contact_id, $tag_id ) { // Sync the tag change to an external system.}, 10, 2 );Register a custom form field type
Section titled “Register a custom form field type”Add a class that implements the field interface, then register it on
primocrm_form_field_types.
add_filter( 'primocrm_form_field_types', function ( $fields ) { $fields['signature'] = MySignatureField::class; return $fields;} );Your class implements the same interface as the core field types (a type()
method and rendering and sanitizing logic). Use the built-in fields under
includes/modules/forms/Fields/Types as a reference. The new type then appears in
the form builder palette. See Field Types.
Register a migration source
Section titled “Register a migration source”A migration source reads one external system and normalizes it into PrimoCRM
contacts, lists, tags, and custom fields. Implement the source contract and
register it on primocrm_migration_sources.
add_filter( 'primocrm_migration_sources', function ( $sources ) { $sources['mailpoet'] = new MailPoetSource(); return $sources;} );The registry defensively drops anything that does not implement the source interface, so a faulty add-on cannot crash the importer. The Free plugin ships CSV and FluentCRM sources as references. See From Another CRM.
Provide an email sender
Section titled “Provide an email sender”Sending is routed through the primocrm_smtp_send filter. Return a truthy result
to indicate you handled the send, a WP_Error on failure, or the incoming value
to pass the send along. This is exactly how the Pro ESP providers plug in.
add_filter( 'primocrm_smtp_send', function ( $result, $provider, $atts, $settings ) { if ( $provider !== 'myesp' ) { return $result; // not ours; let the next handler decide } $ok = my_esp_send( $atts['to'], $atts['subject'], $atts['message'], $settings ); return $ok ? true : new WP_Error( 'myesp_failed', 'Send failed' );}, 10, 4 );When no handler claims the send, PrimoCRM falls back to the default WordPress mailer, so an unconfigured or lapsed site still delivers. See Sending and Deliverability.
Add condition-builder fields and operators
Section titled “Add condition-builder fields and operators”Extend the automation condition builder by filtering primocrm_condition_options.
The value has a fields array and an operators array.
add_filter( 'primocrm_condition_options', function ( $opts ) { $opts['fields'][] = [ 'key' => 'life_time_value', 'label' => 'Lifetime value', 'type' => 'number', ]; $opts['operators'][] = [ 'key' => '>', 'label' => 'Greater than' ]; return $opts;} );This is how Pro adds its commerce fields. See Conditions and Branching.
Register an automation trigger
Section titled “Register an automation trigger”Triggers are collected through primocrm_automation_triggers and appear in the
automation picker. The supported path is to extend BaseTrigger, which
self-registers into that filter for you. This is exactly how the built-in Contact
Form 7, WPForms, and Fluent Forms integrations work.
Extend BaseTrigger, set a unique triggerName, describe the trigger in
getTrigger(), and hook your own event. When the event fires, resolve the contact
and enter it into any automation using your trigger.
use PrimoCRM\modules\automations\Triggers\BaseTrigger;use PrimoCRM\modules\automations\Triggers\Forms\ResolvesContactFromForm;use PrimoCRM\modules\automations\AutomationProcessor;
class SuperFormsSubmittedTrigger extends BaseTrigger{ use ResolvesContactFromForm; // find-or-create a contact by email
public function __construct() { $this->triggerName = 'superforms_submitted'; $this->actionArgNum = 1; parent::__construct(); // self-registers into primocrm_automation_triggers
// Hook YOUR form plugin's own submit action: add_action( 'superforms_after_submit', [ $this, 'onSubmit' ], 10, 1 ); }
// Metadata that lists the trigger in the automation picker. public function getTrigger() { return [ 'category' => 'Forms', 'label' => 'SuperForms Submitted', 'description' => 'Fires when a SuperForms form is submitted', 'icon' => 'document-text', ]; }
public function onSubmit( $submission ) { $email = $submission['email'] ?? ''; $first = $submission['first_name'] ?? ''; $last = $submission['last_name'] ?? '';
// Resolve or create the contact (fires primocrm_contact_created on create). $contact_id = $this->resolveContactFromForm( $email, $first, $last, 'superforms' ); if ( ! $contact_id ) { return; }
// Enter the contact into every active automation using this trigger. $processor = AutomationProcessor::getInstance(); foreach ( $processor->findAutomationsByTrigger( $this->triggerName ) as $automation ) { $processor->enterContact( $automation->id, $contact_id, $this->triggerName ); } }
public function handle( ...$args ) {}}
// Instantiate once, after your form plugin has loaded, so it self-registers.add_action( 'plugins_loaded', function () { new SuperFormsSubmittedTrigger();}, 20 );Your trigger now shows up in the automation builder, and a submission creates or updates the PrimoCRM contact and runs any matching automation.
Make the trigger available on the Free plan
Section titled “Make the trigger available on the Free plan”New trigger keys are treated as Pro by default (fail closed), so they show locked in Free. If your integration should work on the Free plan, add its key to the Free allowlist:
add_filter( 'primocrm_free_triggers', function ( $keys ) { $keys[] = 'superforms_submitted'; return $keys;} );Registering an action
Section titled “Registering an action”Custom actions follow the same shape against primocrm_automation_actions
(extend the base action class, describe it, and implement its step). Use the core
actions under includes/modules/automations/Actions as working references, and
add the key to primocrm_free_actions if it should be free.
Integrating from an external system
Section titled “Integrating from an external system”If the integrating system is not running on the same WordPress site, use the
REST API instead of a trigger class. Create or update a
contact with an authenticated POST /wp-json/primocrm/v1/contacts. Do not write
to PrimoCRM’s database tables directly; the REST endpoint and the trigger classes
are the supported surfaces and keep your integration working across updates.
Choose whether your feature is free
Section titled “Choose whether your feature is free”Anything not on the Free allowlist is treated as Pro (fail closed). If you add a trigger or action that should be available on the Free plan, add its key to the allowlist.
add_filter( 'primocrm_free_triggers', function ( $keys ) { $keys[] = 'my_custom_trigger'; return $keys;} );The same pattern applies to primocrm_free_actions and, for count caps,
primocrm_free_limits.
Inject admin UI
Section titled “Inject admin UI”To add a Settings tab or a campaign row action to the admin app, use the
JavaScript wp.hooks filters primocrm.settings.tabs and
primocrm.campaigns.rowActions. These return descriptors with mount callbacks
rather than React elements, so your UI can render into its own root regardless of
React version. See Actions and Filters.