CLI Commands
Courier's automation commands are designed to run on a schedule — each one is stateless, processes a bounded batch, and logs its results.
For interactive management of contacts, campaigns, segments, tags, and drip enrollments, see Management Commands.
Scheduling
Use CodeIgniter Tasks to run these commands, with singleInstance() so an overlapping run is skipped rather than started:
// app/Config/Tasks.php
$schedule->command('courier:process-drips')->everyMinute()->singleInstance();
$schedule->command('courier:send-campaign')->everyFiveMinutes()->singleInstance();
If you're staying on raw crontab, wrap each command in flock so two overlapping invocations can't run at once:
courier:process-drips also claims enrollments before sending, so duplicate sends are prevented even without singleInstance()/flock — see Drip Sequences. Scheduler-level overlap protection is still recommended as defence in depth.
courier:send-campaign
Sends all scheduled blast campaigns whose scheduled_at time has passed.
To send a specific campaign by ID (useful for testing or manual sends):
What it does:
- Finds all campaigns with status
scheduledandscheduled_at <= now(or just the one you specified) - Sets each campaign to
sending - Resolves the audience (all subscribers, or filtered by segment/tags)
- Sends emails in batches of
$batchSize - Marks the campaign
sentwhen done, orpausedif an exception occurs
A paused campaign can be resumed via CampaignService::resume() or by fixing the issue and calling courier:send-campaign <id> again.
Recommended schedule (see Scheduling):
// app/Config/Tasks.php
$schedule->command('courier:send-campaign')->everyFiveMinutes()->singleInstance();
# raw cron equivalent
*/5 * * * * flock -n /var/lock/courier-send-campaign.lock php /path/to/app/spark courier:send-campaign
courier:process-drips
Sends due drip sequence steps to enrolled contacts.
What it does:
- Reclaims any
processingenrollment stuck past$staleLockMinutes(a crashed prior run) back toactive - Claims active enrollments where
next_send_at <= now(up to$batchSize), marking themprocessingso an overlapping run can't claim them too - Sends the current step email to each contact
- Advances each enrollment to the next step (updating
next_send_atbased on the next step'sdelay_hours) and clears the claim - Marks the enrollment
completedwhen the contact finishes all steps - Cancels the enrollment if the contact is no longer subscribed
Recommended schedule (see Scheduling):
Run this frequently so drip steps go out close to their scheduled time:
// app/Config/Tasks.php
$schedule->command('courier:process-drips')->everyMinute()->singleInstance();
# raw cron equivalent
* * * * * flock -n /var/lock/courier-drips.lock php /path/to/app/spark courier:process-drips
If your batch size is smaller than your active enrollment count, the queue drains across successive runs. That's by design — it prevents overwhelming your email provider in a single burst.
courier:track-events
A stub command for processing bounce webhooks or SMTP feedback loops.
Out of the box this command does nothing but log a warning. It's a placeholder — extend it when you're ready to handle bounce webhooks from your email provider:
<?php
// app/Commands/ProcessBounces.php
namespace App\Commands;
use Myth\Courier\Commands\TrackEvents;
use Myth\Courier\Models\ContactModel;
use Myth\Courier\Enums\ContactStatus;
class ProcessBounces extends TrackEvents
{
protected $name = 'courier:track-events'; // override the base command
protected function processBounce(string $email): void
{
$contact = model(ContactModel::class)->where('email', $email)->first();
if ($contact === null) { return; }
model(ContactModel::class)->update($contact->id, [
'status' => ContactStatus::Bounced->value,
]);
}
}
courier:validate-campaigns
Validates all YAML drip campaign definition files without writing to the database.
Exit code 0 means all files passed (or no files were found). Exit code 1 means at least one file failed. This makes the command safe to use as a pre-deploy gate in CI:
# .github/workflows/ci.yml
- name: Validate campaign files
run: php spark courier:validate-campaigns
See File-Based Campaigns for the full YAML format.
courier:sync-campaigns
Syncs YAML drip campaign files into the courier_campaigns table. Campaigns are upserted by name — running the command twice is safe.
CREATED welcome-sequence.yaml → campaign 'Welcome Sequence'
UPDATED re-engagement.yaml → campaign 'Re-engagement Track'
SKIP onboarding.yaml: step[1]: missing required field 'subject'
Invalid files are skipped with a SKIP error; valid files in the same batch still sync. Steps are not written to the database — the YAML file is the runtime source of truth for step content and is read at send time by courier:process-drips.
Run this command during deployment after any campaign file changes.
Logging
All automation commands write to CI4's log system using the [Courier] prefix. Check your writable/logs/ directory if something isn't sending as expected.