Start here
Your first add-on
Build a small WordPress plugin that adds one automation trigger and one automation action to Marker CRM.
# Your first add-on This page builds **Acme Rewards**, a plugin that tells Marker CRM when a member reaches a reward level, and gives automations a step that grants them points. When you are done, a site owner can build "When a member reaches Gold → wait 1 day → grant 100 points → send an email" without writing any code. You need a WordPress site with Marker CRM active. ## 1. Create the plugin Make a folder `wp-content/plugins/acme-rewards/` with one file, `acme-rewards.php`: ```php <?php /** * Plugin Name: Acme Rewards for Marker CRM * Description: Reward levels and points, as Marker CRM automation steps. * Version: 1.0.0 * Requires PHP: 8.0 * Text Domain: acme-rewards */ defined('ABSPATH') || exit; use MarkerCRM\App\Models\Contact; use MarkerCRM\App\Services\Automation\RunContext; use MarkerCRM\App\Services\Automation\StepResult; use MarkerCRM\App\Services\Contact\ContactService; add_action('marker_crm/loaded', function () { add_filter('marker_crm/automation_triggers', 'acme_rewards_triggers'); add_filter('marker_crm/automation_actions', 'acme_rewards_actions'); }); ``` Everything is registered inside `marker_crm/loaded`, so if Marker CRM is switched off the plugin quietly does nothing. The `use` lines do not load anything by themselves, so they are safe even then. ## 2. Register a trigger A trigger is an entry in the `marker_crm/automation_triggers` array, keyed by a name made of lowercase letters, numbers, dashes and underscores. Prefix it with your plugin's slug so it cannot collide with anyone else's. ```php function acme_rewards_triggers(array $triggers): array { $triggers['acme_level_reached'] = [ 'label' => __('Member reaches a level', 'acme-rewards'), 'description' => __('Runs when a member reaches the chosen reward level.', 'acme-rewards'), 'group' => __('Rewards', 'acme-rewards'), 'integration' => ['key' => 'acme-rewards', 'label' => 'Acme Rewards'], 'icon' => 'trophy', 'fields' => [[ 'key' => 'level', 'label' => __('Level', 'acme-rewards'), 'type' => 'select', 'required' => true, 'options' => [ ['value' => 'silver', 'label' => __('Silver', 'acme-rewards')], ['value' => 'gold', 'label' => __('Gold', 'acme-rewards')], ], ]], 'matches' => fn (array $settings, array $args, array $context) => ($context['level'] ?? '') === ($settings['level'] ?? ''), ]; return $triggers; } ``` - `fields` describes the settings panel. The automation builder draws it for you — an add-on ships no JavaScript. See [Settings fields](/developers/automations/fields/). - `matches` decides whether a particular automation should start. `$settings` is what the site owner chose in that automation; `$context` is what you pass when you fire the trigger. - `integration` puts your trigger under its own entry in the trigger picker. Leave it out and the trigger is filed under **Other**. ## 3. Fire the trigger When a member reaches a level in your own code, find their contact and fire the trigger by name through `marker_crm/trigger`: ```php function acme_rewards_level_reached(int $userId, string $level): void { $user = get_userdata($userId); if (!$user || !did_action('marker_crm/loaded')) { return; } $contact = ContactService::matching([['channel' => 'email', 'address' => $user->user_email]]); if ($contact) { do_action('marker_crm/trigger', 'acme_level_reached', $contact, ['level' => $level]); } } ``` The `did_action()` check keeps the function safe to call while Marker CRM is inactive, when `ContactService` does not exist. `marker_crm/trigger` takes the trigger name, a `Contact` (or a contact id), and a **context** array. The context is stored on every run the trigger starts, so later steps and email merge tags can read it. Keep it flat and scalar — strings, numbers, ids — never objects. :::note This example only reaches people who are already contacts. To create a contact when someone new reaches a level, call `ContactService::createOrUpdate()` and give them the `transactional` status — reaching a level is not consent to marketing email. [Concepts](/developers/start/concepts/#subscriptions-and-consent) explains why. ::: ## 4. Register an action An action is an entry in `marker_crm/automation_actions` with a `run` callable: ```php function acme_rewards_actions(array $actions): array { $actions['acme_grant_points'] = [ 'label' => __('Grant reward points', 'acme-rewards'), 'description' => __('Adds points to the member’s account.', 'acme-rewards'), 'group' => __('Rewards', 'acme-rewards'), 'integration' => ['key' => 'acme-rewards', 'label' => 'Acme Rewards'], 'icon' => 'gift', 'fields' => [[ 'key' => 'points', 'label' => __('Points', 'acme-rewards'), 'type' => 'number', 'default' => 100, 'required' => true, ]], 'sanitize' => fn (array $settings) => ['points' => min(10000, absint($settings['points'] ?? 0))], 'run' => 'acme_rewards_grant_points', ]; return $actions; } function acme_rewards_grant_points(Contact $contact, array $settings, RunContext $run): StepResult { $userId = (int) $contact->user_id; if (!$userId) { return StepResult::skipped(__('The contact has no WordPress account to add points to.', 'acme-rewards')); } $points = (int) get_user_meta($userId, 'acme_points', true); update_user_meta($userId, 'acme_points', $points + (int) $settings['points']); return StepResult::done(); } ``` - `run` receives the contact, the step's saved settings and a `RunContext`. Read the trigger's context with `$run->get('level')`. - It returns a `StepResult` — or nothing, which means done. `skipped()` moves on and records why; `fail()` retries after 1 and then 5 minutes, and the third failure marks the run **Failed**. See [Actions](/developers/automations/actions/). - `sanitize` owns the step's settings when you give it. Without it, every value is cleaned as plain text. ## 5. Try it 1. Activate **Acme Rewards for Marker CRM** under **Plugins**. 2. In Marker CRM, open **Automations**, create an automation and choose the trigger. Your plugin appears in the picker's rail as **Acme Rewards**, with **Member reaches a level** inside it. 3. Choose **Gold**, add the **Grant reward points** step, and turn the automation on. 4. Call `acme_rewards_level_reached($userId, 'gold')` for a user who is a contact — from WP-CLI, `wp eval 'acme_rewards_level_reached(1, "gold");'` works. 5. Open the automation's report. The contact is listed, and once the queue has run, the user has 100 more `acme_points`. ## When it does not work - **The trigger is not in the picker.** Check your debug log for a `_doing_it_wrong` notice naming your trigger: an entry with a bad name or a callable that is not callable is skipped, and the notice says why. - **Nobody enters.** The automation must be on, the contact must exist, and `matches` must return `true` for the settings that automation saved. - **Entered, but the step never runs.** Actions run on Marker CRM's queue. Check that it is running in **Settings → Tools**. - **The run is Failed.** The run's history in the automation report shows the reason your action gave, or the exception it threw. ## Next - [Triggers](/developers/automations/triggers/) — listening to a WordPress hook instead of firing by name, bulk events, creating contacts. - [Goals and events](/developers/automations/goals-and-events/) — add `met` so your trigger can also be a goal.This page builds Acme Rewards, a plugin that tells Marker CRM when a member reaches a reward level, and gives automations a step that grants them points. When you are done, a site owner can build “When a member reaches Gold → wait 1 day → grant 100 points → send an email” without writing any code.
You need a WordPress site with Marker CRM active.
1. Create the plugin
Section titled “1. Create the plugin”Make a folder wp-content/plugins/acme-rewards/ with one file, acme-rewards.php:
<?php/** * Plugin Name: Acme Rewards for Marker CRM * Description: Reward levels and points, as Marker CRM automation steps. * Version: 1.0.0 * Requires PHP: 8.0 * Text Domain: acme-rewards */
defined('ABSPATH') || exit;
use MarkerCRM\App\Models\Contact;use MarkerCRM\App\Services\Automation\RunContext;use MarkerCRM\App\Services\Automation\StepResult;use MarkerCRM\App\Services\Contact\ContactService;
add_action('marker_crm/loaded', function () { add_filter('marker_crm/automation_triggers', 'acme_rewards_triggers'); add_filter('marker_crm/automation_actions', 'acme_rewards_actions');});Everything is registered inside marker_crm/loaded, so if Marker CRM is switched off the
plugin quietly does nothing. The use lines do not load anything by themselves, so they are
safe even then.
2. Register a trigger
Section titled “2. Register a trigger”A trigger is an entry in the marker_crm/automation_triggers array, keyed by a name made of
lowercase letters, numbers, dashes and underscores. Prefix it with your plugin’s slug so it
cannot collide with anyone else’s.
function acme_rewards_triggers(array $triggers): array{ $triggers['acme_level_reached'] = [ 'label' => __('Member reaches a level', 'acme-rewards'), 'description' => __('Runs when a member reaches the chosen reward level.', 'acme-rewards'), 'group' => __('Rewards', 'acme-rewards'), 'integration' => ['key' => 'acme-rewards', 'label' => 'Acme Rewards'], 'icon' => 'trophy', 'fields' => [[ 'key' => 'level', 'label' => __('Level', 'acme-rewards'), 'type' => 'select', 'required' => true, 'options' => [ ['value' => 'silver', 'label' => __('Silver', 'acme-rewards')], ['value' => 'gold', 'label' => __('Gold', 'acme-rewards')], ], ]], 'matches' => fn (array $settings, array $args, array $context) => ($context['level'] ?? '') === ($settings['level'] ?? ''), ];
return $triggers;}fieldsdescribes the settings panel. The automation builder draws it for you — an add-on ships no JavaScript. See Settings fields.matchesdecides whether a particular automation should start.$settingsis what the site owner chose in that automation;$contextis what you pass when you fire the trigger.integrationputs your trigger under its own entry in the trigger picker. Leave it out and the trigger is filed under Other.
3. Fire the trigger
Section titled “3. Fire the trigger”When a member reaches a level in your own code, find their contact and fire the trigger by
name through marker_crm/trigger:
function acme_rewards_level_reached(int $userId, string $level): void{ $user = get_userdata($userId);
if (!$user || !did_action('marker_crm/loaded')) { return; }
$contact = ContactService::matching([['channel' => 'email', 'address' => $user->user_email]]);
if ($contact) { do_action('marker_crm/trigger', 'acme_level_reached', $contact, ['level' => $level]); }}The did_action() check keeps the function safe to call while Marker CRM is inactive, when
ContactService does not exist. marker_crm/trigger takes the trigger name, a Contact (or a contact id), and a context
array. The context is stored on every run the trigger starts, so later steps and email merge
tags can read it. Keep it flat and scalar — strings, numbers, ids — never objects.
4. Register an action
Section titled “4. Register an action”An action is an entry in marker_crm/automation_actions with a run callable:
function acme_rewards_actions(array $actions): array{ $actions['acme_grant_points'] = [ 'label' => __('Grant reward points', 'acme-rewards'), 'description' => __('Adds points to the member’s account.', 'acme-rewards'), 'group' => __('Rewards', 'acme-rewards'), 'integration' => ['key' => 'acme-rewards', 'label' => 'Acme Rewards'], 'icon' => 'gift', 'fields' => [[ 'key' => 'points', 'label' => __('Points', 'acme-rewards'), 'type' => 'number', 'default' => 100, 'required' => true, ]], 'sanitize' => fn (array $settings) => ['points' => min(10000, absint($settings['points'] ?? 0))], 'run' => 'acme_rewards_grant_points', ];
return $actions;}
function acme_rewards_grant_points(Contact $contact, array $settings, RunContext $run): StepResult{ $userId = (int) $contact->user_id;
if (!$userId) { return StepResult::skipped(__('The contact has no WordPress account to add points to.', 'acme-rewards')); }
$points = (int) get_user_meta($userId, 'acme_points', true); update_user_meta($userId, 'acme_points', $points + (int) $settings['points']);
return StepResult::done();}runreceives the contact, the step’s saved settings and aRunContext. Read the trigger’s context with$run->get('level').- It returns a
StepResult— or nothing, which means done.skipped()moves on and records why;fail()retries after 1 and then 5 minutes, and the third failure marks the run Failed. See Actions. sanitizeowns the step’s settings when you give it. Without it, every value is cleaned as plain text.
5. Try it
Section titled “5. Try it”- Activate Acme Rewards for Marker CRM under Plugins.
- In Marker CRM, open Automations, create an automation and choose the trigger. Your plugin appears in the picker’s rail as Acme Rewards, with Member reaches a level inside it.
- Choose Gold, add the Grant reward points step, and turn the automation on.
- Call
acme_rewards_level_reached($userId, 'gold')for a user who is a contact — from WP-CLI,wp eval 'acme_rewards_level_reached(1, "gold");'works. - Open the automation’s report. The contact is listed, and once the queue has run, the user
has 100 more
acme_points.
When it does not work
Section titled “When it does not work”- The trigger is not in the picker. Check your debug log for a
_doing_it_wrongnotice naming your trigger: an entry with a bad name or a callable that is not callable is skipped, and the notice says why. - Nobody enters. The automation must be on, the contact must exist, and
matchesmust returntruefor the settings that automation saved. - Entered, but the step never runs. Actions run on Marker CRM’s queue. Check that it is running in Settings → Tools.
- The run is Failed. The run’s history in the automation report shows the reason your action gave, or the exception it threw.
- Triggers — listening to a WordPress hook instead of firing by name, bulk events, creating contacts.
- Goals and events — add
metso your trigger can also be a goal.