---
title: "Booking approvals — Appointments for Laravel | Roundly"
description: "Require sign-off before a booking is confirmed — flat, quorum, staged or preset workflows that drive the appointment status."
url: https://roundly-consulting.com/open-source/docs/appointments-for-laravel/booking-approvals
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Appointments for Laravel](https://roundly-consulting.com/open-source/docs/appointments-for-laravel.md)

# Booking approvals

Opt a booking into sign-off with approvals-for-laravel. The appointment is created pending and an approval request is opened; the SyncAppointmentStatusFromApproval listener then moves the appointment as the request resolves:

```php
use RoundlyConsulting\Appointments\Facades\Appointments;
use RoundlyConsulting\Approvals\Enums\ApprovalRule;
use RoundlyConsulting\Approvals\Facades\Approvals;

$appointment = Appointments::schedule('Booking request')
    ->startingAt('2026-07-01 09:00')
    ->requireApprovalFrom([$organiser, $clinician], ApprovalRule::Quorum, quorum: 1)
    ->create();                                  // created pending, approval request opened

Approvals::for($appointment)->as($organiser)->approve();   // → confirmed
// or: Approvals::for($appointment)->as($organiser)->reject();  → declined
```

| Approval request resolves as | Appointment status becomes |
| --- | --- |
| `Approved` | confirmed |
| `Rejected` | declined |
| `Cancelled / Expired` | cancelled |
| `Pending` | unchanged |

## Rules and quorum

requireApprovalFrom() accepts one approver or many, a rule and an optional quorum. The rule defaults to ApprovalRule::Unanimous; approvals-for-laravel also offers Quorum, Any and Weighted:

```php
use RoundlyConsulting\Appointments\Facades\Appointments;
use RoundlyConsulting\Approvals\Enums\ApprovalRule;

Appointments::schedule('Board review')
    ->startingAt('2026-07-01 09:00')
    ->requireApprovalFrom([$chair, $treasurer])     // ApprovalRule::Unanimous by default
    ->create();

Appointments::schedule('Team booking')
    ->startingAt('2026-07-01 09:00')
    ->requireApprovalFrom([$lead, $deputy])
    ->approvalRule(ApprovalRule::Quorum)            // standalone setters
    ->approvalQuorum(1)
    ->create();
```

## Staged pipelines and presets

Stages open one after another — each only once the previous one clears. Named presets come from config('approvals.workflows'):

```php
use RoundlyConsulting\Approvals\DataTransferObjects\StageDefinition;

Appointments::schedule('Two-desk booking')
    ->startingAt('2026-07-01 09:00')
    ->approvalStages([
        new StageDefinition([$reception]),
        new StageDefinition([$clinician]),
    ])
    ->rejectOnStageRejection()                   // default: a rejected stage declines the booking
    ->create();

Appointments::schedule('Preset booking')
    ->startingAt('2026-07-01 09:00')
    ->approvalWorkflow('clinic')                 // config('approvals.workflows.clinic')
    ->approvalStageApprovers([[$reception], [$clinician]])
    ->create();
```

```php
// config/approvals.php
'workflows' => [
    'clinic' => [
        'stages' => [
            ['rule' => 'unanimous', 'required_approvers' => 1, 'name' => 'reception'],
            ['rule' => 'any', 'required_approvers' => 1, 'name' => 'clinician'],
        ],
    ],
],
```

- Precedence: a named workflow preset wins, then explicit stages, then the flat approver set.
- rejectOnStageRejection() is on by default — a rejected stage declines the whole booking.
- approvalStageApprovers() supplies one approver group per stage for a staged preset; a flat preset takes the requireApprovalFrom() approvers.

## Approver models

Only the named approvers can decide. Models that decide use the approvals GivesApprovals trait:

```php
use Illuminate\Foundation\Auth\User as Authenticatable;
use RoundlyConsulting\Appointments\Concerns\HasAppointments;
use RoundlyConsulting\Approvals\Traits\GivesApprovals;

class User extends Authenticatable
{
    use GivesApprovals;
    use HasAppointments;
}
```

## What the sync listener does

- Fires AppointmentStatusChanged like any other transition, so your listeners don’t care how the decision arrived.
- Records the deciding actor in meta under approval\_decided\_by, as a type (morph class) and id.
- Ignores repeated resolutions and requests whose subject isn’t an appointment.
- Never leaves a final status — cancelled, declined, completed or no\_show — either way: approving the still-open request of a booking the customer already cancelled changes nothing.
- With approvals.enforce\_transitions on, a mapped move the transition matrix forbids is skipped. Off (the default), the mapped status is forced between live statuses — confirmed → declined included.

## Approval state on the appointment

```php
$appointment->isPendingApproval();         // bool
$appointment->isApproved();                // bool
$appointment->currentStage()?->name;       // staged pipelines: the open stage, e.g. 'reception'
$appointment->approvalRequests()->count(); // the approval requests opened for this booking
$appointment->meta?->get('approval_decided_by'); // ['type' => …, 'id' => …] once decided
```

## Expiring decisions

appointments:expire-approvals runs the approvals engine’s expiry scoped to appointments — the configured appointments.model, by its morph-map alias when you register one. It lapses expired appointment approval decisions and requests only, leaving other subjects’ approvals alone, and the appointments whose requests resolve move to cancelled through the listener:

```bash
php artisan appointments:expire-approvals
# Lapsed 3 expired approval decision(s).
```

Run it on a schedule:

```php
// routes/console.php
use Illuminate\Support\Facades\Schedule;

Schedule::command('appointments:expire-approvals')->everyFiveMinutes();
```

## 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](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## 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.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/appointments-for-laravel.md)
