Skip to content

Extending the CRM

Add a new kind of named group beside tags and lists, and what Marker CRM does with it.

Tags and lists are both terms, told apart by their type. The set of types is a filter, so an add-on can add its own — a membership tier, a course cohort — and contacts can hold terms of that type the way they hold tags.

add_action('marker_crm/loaded', function () {
add_filter('marker_crm/term_types', function (array $types) {
$types[] = 'acme_tier';
return $types;
});
});

Use a key of lowercase letters, numbers and underscores, prefixed with your plugin’s slug.

Once the type is registered:

REST GET terms?type=acme_tier lists them and POST terms with type creates one; PUT and DELETE terms/{id} work on any term. An unregistered type is read as tag, and the success messages use the tag wording
Contacts ContactService::createOrUpdate() and the contacts REST endpoints accept acme_tiers (the type plus s): an array of term ids
Contact responses Each contact carries acme_tiers, the ids it holds, when its terms are loaded
Segments and conditions A rule field acme_tiers on the Segmentation tab, with is any of, is none of, has all of, is set and is not set
Admin boot data The type’s terms are sent to the admin with the tags and lists, up to the boot limit
Events marker_crm/term_attached and marker_crm/term_detached
use MarkerCRM\App\Services\Contact\TermService;
$gold = TermService::createOrUpdate(['type' => 'acme_tier', 'name' => 'Gold']);

A slug is made from the name and kept unique within the type. Colours belong to tags and public/private visibility to lists, so a term of a new type has neither.

Over REST: POST /wp-json/markercrm/v2/terms with type and name. It needs the edit_segments capability.

Pass the full set of the type’s term ids for the contact:

use MarkerCRM\App\Services\Contact\ContactService;
ContactService::createOrUpdate(['id' => $contact->id, 'acme_tiers' => [$gold->id]]);

The array replaces the contact’s terms of that type: ids left out are removed. Leave the key out entirely to leave them alone. Ids that belong to a different type are ignored.

For each term added and each removed, Marker CRM fires:

Hook Arguments
marker_crm/term_attached Model $object, Term $term
marker_crm/term_detached Model $object, Term $term

$object is the contact. Check $term->type — these hooks fire for every add-on type, not only yours. Tags and lists fire their own names instead (tag_added_to_contact, contact_added_to_list and the rest).

By default, removing a term deletes the relation, as with tags. A list instead keeps it with status: unsubscribed, so the CRM remembers the person opted out. To give your type the same behaviour, add it to marker_crm/stateful_term_types:

add_filter('marker_crm/stateful_term_types', function (array $types) {
$types[] = 'acme_tier';
return $types;
});

The segment field is labelled with the type key, capitalised (Acme_tier). Give it a proper label on marker_crm/segment_fields, which runs after the field is built:

add_filter('marker_crm/segment_fields', function (array $fields) {
if (isset($fields['acme_tiers'])) {
$fields['acme_tiers']['label'] = __('Membership tier', 'acme');
}
return $fields;
});

Past 300 terms of a type, the admin is sent only the newest 300 and searches the server for the rest. marker_crm/term_boot_limit changes that number for every type.

  • No automation triggers. Tag is added and Added to a list are for tags and lists only; register your own trigger on marker_crm/term_attached if you need one.
  • No filter on the contacts list screen, and no bulk add or remove from it.
  • Deleting a term (TermService::delete()) removes it from every contact silently. Nothing is fired per contact.