NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Campaigns for Laravel

Custom delivery jobs

To deliver over any other transport — an SMS gateway, a push service, a webhook — write your own job, implement Contracts\ProcessesCampaignRecipient and register it:

// config/campaigns.php
'process-recipient-job' => \App\Jobs\SendCampaignSms::class,

The contract fixes the constructor — (Campaign $campaign, CampaignRecipient $recipient) — and handle(CampaignManager $campaigns): void. The injected manager is the one behind the Campaigns facade, so record each outcome through the campaign’s handle:

namespace App\Jobs;

use App\Services\SmsGateway; // your own SMS client
use Illuminate\Bus\Batchable;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use RoundlyConsulting\Campaigns\Campaign;
use RoundlyConsulting\Campaigns\CampaignRecipient;
use RoundlyConsulting\Campaigns\CampaignManager;
use RoundlyConsulting\Campaigns\Contracts\ProcessesCampaignRecipient;
use Throwable;

final class SendCampaignSms implements ProcessesCampaignRecipient, ShouldQueue
{
    use Batchable, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(public Campaign $campaign, public CampaignRecipient $recipient) {}

    public function handle(CampaignManager $campaigns): void
    {
        if ($this->batch()?->cancelled()) {
            return; // the campaign was cancelled
        }

        try {
            app(SmsGateway::class)->send(
                $this->recipient->reachableAt,
                strip_tags($this->campaign->content),
            );

            $campaigns->campaign($this->campaign)->markProcessed($this->recipient);
        } catch (Throwable $e) {
            $campaigns->campaign($this->campaign)->markFailed($this->recipient, $e->getMessage());
        }
    }
}

Checklist

  • Use Illuminate\Bus\Batchable — start() adds your job to the campaign’s batch.
  • Implement ShouldQueue so recipients are processed by queue workers.
  • Return early when $this->batch()?->cancelled() is true, so a cancelled campaign stops sending.
  • Record the outcome — catch exceptions and call ->markFailed($recipient, $message) on the handle, as the shipped jobs do — or let the job fail: a job that fails outright (throws, times out, runs out of attempts) counts as a failed delivery while the other recipients keep sending. Never both, or the recipient counts as failed twice.
  • Call ->markProcessed($recipient) on success — it records the delivery and fires RecipientProcessed.
  • Under Campaigns::fake() both calls are recorded, so assertRecipientProcessed() and assertRecipientFailed() test your job (see Testing).

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.