Skip to content

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.

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 );

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.

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.

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.

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.

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.

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.

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.

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.