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.
When the host plugin is not installed
Section titled “When the host plugin is not installed”There is nothing to unlock: a registered trigger is a working trigger. What you
do need to handle is your own host plugin being absent. Register the trigger
anyway and give it a requires_plugin string, and PrimoCRM lists it as
unavailable, naming what it needs, instead of leaving a silent gap in the picker:
add_filter( 'primocrm_automation_triggers', function ( $triggers ) { if ( isset( $triggers['superforms_submitted'] ) || defined( 'SUPERFORMS_VERSION' ) ) { return $triggers; } $triggers['superforms_submitted'] = array( 'label' => 'Super Forms Submitted', 'category' => 'Forms', 'description' => 'Fires when a Super Forms form is submitted', 'requires_plugin' => 'Super Forms', ); return $triggers;}, 99 );Run at a late priority so the real trigger, registered at 10, always wins.
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.
The same requires_plugin rule applies when your action needs a host plugin.
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.
Inject admin UI
Section titled “Inject admin UI”The admin app is a React single-page app, and wp.hooks filters are how you put
your own screens inside it. There are seams for Settings tabs, Reports tabs,
campaign row actions, campaign wizard steps, campaign detail panels and email
editors, all listed in Actions and Filters.
Each one returns descriptors with mount callbacks rather than React elements, so your UI renders into its own root and your React version does not have to match PrimoCRM’s. Do not add a standalone admin page; use the seams, and your feature lands where the user is already working.