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

Each transition dispatches an event carrying the relevant model. All events live in RoundlyConsulting\Approvals\Events and expose their payload as public properties:

EventPayloadFired when
ApprovalRequestedapprovala pending decision is asked for (ask())
ApprovalApprovedapprovala decision is approved
ApprovalRejectedapprovala decision is rejected
ApprovalCancelledapprovala decision is withdrawn — by cancel(), by a toggle-off, or an outstanding ask retired because its request resolved, its stage settled or your app closed the round (close())
ApprovalExpiredapprovala pending or approved decision lapses
ApprovalRequestResolvedrequesta request reaches approved, rejected or expired — or your app closes it as cancelled or expired (close()); once, after its outstanding asks are retired
ApprovalToggledactor, entity, hasBeenApprovedtoggle() / toggleApproval() runs
ApprovalStageOpenedstagea staged request opens a stage
ApprovalStageClearedstagea stage clears as approved
ApprovalDelegateddelegationan approver delegates authority
ApprovalDelegationRevokeddelegationa delegation is revoked
ApprovalStatusChangedsubject, from, to, actorumbrella — every status transition of a decision or request, alongside the granular event

The umbrella event

Subscribe to ApprovalStatusChanged once to observe all transitions: a decision approved, rejected, withdrawn — including by a toggle or by a change of mind that supersedes it — or expired, an outstanding ask retired when its round closes, and a request resolved, expired or closed by your app. It carries the subject (the Approval or ApprovalRequest that changed), the from and to ApprovalStatus, and the actor when one is known — null for a resolved request or a retired ask:

use RoundlyConsulting\Approvals\Events\ApprovalStatusChanged;

class AuditApprovalChanges
{
    public function handle(ApprovalStatusChanged $event): void
    {
        // $event->from, $event->to, $event->subject, $event->actor
        logger()->info("approval moved {$event->from->value} -> {$event->to->value}");
    }
}

Granular listeners

use Illuminate\Support\Facades\Event;
use RoundlyConsulting\Approvals\Events\ApprovalApproved;
use RoundlyConsulting\Approvals\Events\ApprovalRequestResolved;

Event::listen(function (ApprovalApproved $event): void {
    $approval = $event->approval;   // the Approval model
});

Event::listen(function (ApprovalRequestResolved $event): void {
    $request = $event->request;     // approved, rejected or expired — see $request->status
    $subject = $request->subject;   // e.g. the Release
});

An ask() creates a pending decision rather than changing one, so it fires only ApprovalRequested. Stage changes and delegation also fire only their own events — not the umbrella one.

When a round closes

Whether its decisions resolve it, it lapses or your app closes it, a round’s events come in one order: ApprovalCancelled and ApprovalStatusChanged for each retired ask, then ApprovalRequestResolved, then ApprovalStatusChanged for the request. A listener of the resolution never sees an ask still waiting on the round.

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.