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.

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

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.

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.

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.

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.