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

The Approvals facade

Everything goes through one API — the Approvals facade, the injectable ApprovalsManager behind it, or the action classes it runs. All three execute the same code, and the facade is the recommended entry point. It is registered automatically. The full surface:

use RoundlyConsulting\Approvals\Facades\Approvals;

// One actor's decision on one approvable
Approvals::for($deployment)->as($user)->because('Looks good to me')->approve();
Approvals::for($deployment)->as($user)->because('Please add tests')->reject(); // withdraws the approval above
Approvals::for($deployment)->as($user)->ask();                 // ask the actor: a pending decision
Approvals::for($deployment)->as($user)->cancel();              // withdraw the actor's live decision
Approvals::for($deployment)->as($user)->toggle();              // simple on/off
Approvals::for($deployment)->as($user)->expiresIn(86400)->approve(); // valid for a day
Approvals::for($deployment)->as($user)->weight(3)->approve();  // override the decision's weight
Approvals::for($invoice)->as($user)->within($request)->approve(); // pin to one request

// Reads
Approvals::for($deployment)->as($user)->isApproved();  // bool
Approvals::for($deployment)->as($user)->isRejected();  // bool
Approvals::for($deployment)->hasPending();             // bool — any pending decision

// Multi-approver requests (flat, staged, or from a workflow preset)
Approvals::request($invoice)->from([$a, $b, $c])->quorum(2)->expiresIn(3600)->open();
Approvals::request($release)->stages([...])->continueOnRejection()->expiringAt($t)->open();
Approvals::request($budget)->workflow('payout')->open([$a, $b, $c]);

Approvals::status($invoice);        // ApprovalStatus of the latest request (Pending when none)
Approvals::progress($invoice);      // ?ApprovalProgress
Approvals::currentStage($release);  // ?ApprovalRequestStage
Approvals::preset('payout');        // WorkflowPreset from config

// Delegation
Approvals::delegations($boss)->to($deputy)->from($monday)->until($friday)->grant();
Approvals::delegations($boss)->revoke($deputy);   // or ->revoke() for all; returns int
Approvals::delegations($boss)->active();          // Collection<ApprovalDelegation>
Approvals::delegationFor($deputy);                // ?ApprovalDelegation in force now

// Housekeeping
Approvals::expire();                // lapse overdue asks, approvals and requests; returns int
Approvals::expire(subjectType: Invoice::class); // only those on invoices (class or morph alias)

Facade methods

MethodReturnsPurpose
for($approvable)PendingApprovalStart a decision on an approvable; set the actor with as().
as($actor)PendingApprovalStart a decision by an actor; set the approvable with for().
request($subject)PendingApprovalRequestOpen a multi-approver request — flat, staged or from a preset.
status($subject)ApprovalStatusStatus of the subject’s latest request; Pending when it has none, Expired once an open request is past its expiry.
progress($subject)?ApprovalProgressProgress snapshot of the latest request.
currentStage($subject)?ApprovalRequestStageThe open stage of the latest staged request.
preset($name)WorkflowPresetResolve and validate a preset from config('approvals.workflows').
delegations($delegator)DelegationsHandleGrant, revoke and list one delegator’s delegations.
delegationFor($delegate, ?$at)?ApprovalDelegationThe delegation the model acts under at $at (now when omitted).
expire(?$now, ?$subjectType)intLapse overdue asks, approvals and requests; returns how many. With $subjectType (a model class or morph alias) only those on that type.
fake()ApprovalsFakeSwap in the recording fake; recorded() and every assert*() are then callable on the facade too (see Testing).

Decisions — PendingApproval

Start from the approvable with for() or from the actor with as(), chain the context, then call a terminal method. A request that names its approvers refuses anyone else with UnauthorizedApprovalException, and a closed request refuses every decision with ClosedApprovalRequestException:

MethodReturnsPurpose
for($approvable)selfSet the model being decided on.
as($actor)selfSet the deciding model.
within($request)selfPin the decision to this request instead of the approvable’s latest open one. It must belong to the approvable (else InvalidApprovalRequestException) and still be open (else ClosedApprovalRequestException).
because($reason)selfAttach a reason — stored by approve(), reject() and cancel().
weight($n)selfOverride the weight this decision carries — approve() and reject().
expiresIn($seconds)selfExpire N seconds from now — applied by approve() and ask().
expiringAt($at)selfAbsolute expiry — applied by approve() and ask().
approve()ApprovalRecord an approval. It counts towards the pinned request, else the approvable’s latest open request; repeating it is a no-op.
reject()ApprovalRecord a rejection — an approval the actor held is withdrawn.
ask()ApprovalAsk the actor for a pending decision; returns the actor’s live decision unchanged when it already holds one.
cancel()?ApprovalWithdraw the actor’s live decision — or one it made as a delegate — in the round a decision would land in; null when there is none.
toggle()boolSimple on/off — true approved (through approve()), false withdrawn.
isApproved()boolWhether the actor holds an approval of the approvable that is still in force.
isRejected()boolWhether the actor holds a rejection for the approvable.
hasPending()boolWhether the approvable has any pending decision not yet past its deadline — needs only for().

Requests — PendingApprovalRequest

MethodReturnsPurpose
from([...])selfThe named approvers of a flat request — only they (or their delegates) can decide it. Each is stored once; required_approvers defaults to their count.
rule($rule, ?$quorum)selfThe flat request’s rule and threshold — Unanimous by default.
any() / quorum($n) / weighted($n)selfShortcuts for rule(Any), rule(Quorum, $n) and rule(Weighted, $n).
stages([StageDefinition, ...])selfMake it a staged request. Combined with from(), open() throws InvalidApprovalRequestException.
continueOnRejection()selfStaged: keep going past a rejected stage.
expiresIn($s) / expiringAt($t)selfStamp expires_at on the request.
open()ApprovalRequestOpen the flat or staged request; a threshold its approvers could never reach is refused.
workflow($name)->open([...])ApprovalRequestOpen from a named preset — a flat list, or one list per stage.

Delegation — DelegationsHandle

MethodReturnsPurpose
delegations($d)->to($delegate)PendingDelegationStart a delegation — nothing is written until grant().
->from($t) / ->until($t) / ->for($seconds)PendingDelegationSet the window; for() counts from the start (now without from()).
->grant()ApprovalDelegationValidate the window, save it and fire ApprovalDelegated.
delegations($d)->revoke(?$delegate)intRevoke this delegator’s active and scheduled delegations — or only those to $delegate.
delegations($d)->active(?$at)Collection<ApprovalDelegation>Delegations in force at $at (now), newest first.

Pinning to a request, the approvals() helper, absolute expiry and incomplete builders are covered under Fluent builders; the same API without the facade is under DI and actions.

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.