---
title: "Workflow presets — Approvals for Laravel | Roundly"
description: "Define a rule, quorum, stage and expiry setup once in config and open requests from it by name — flat or staged."
url: https://roundly-consulting.com/open-source/docs/approvals-for-laravel/workflow-presets
language: en
---

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

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

# Workflow presets

Capture a reusable rule, quorum, stage and expiry setup in config('approvals.workflows') so call sites stay short. A preset is flat (one rule) or staged (a stages list):

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

'workflows' => [
    'payout' => [
        'rule' => ApprovalRule::Quorum->value,
        'quorum' => 2,
        'required_approvers' => 3,
        'expiry' => 86400,
    ],
    'release' => [
        'reject_on_stage_rejection' => true,
        'stages' => [
            ['rule' => ApprovalRule::Unanimous->value, 'required_approvers' => 2, 'name' => 'engineering'],
            ['rule' => ApprovalRule::Any->value,       'required_approvers' => 1, 'name' => 'product'],
        ],
    ],
],
```

## Opening a request from a preset

```php
use RoundlyConsulting\Approvals\Facades\Approvals;

// Flat preset: a single approver list.
Approvals::request($budget)->workflow('payout')->open([$a, $b, $c]);

// Staged preset: one approver group per stage, in order.
Approvals::request($release)->workflow('release')->open([[$eng1, $eng2], [$product]]);

// Read a validated preset without opening anything.
Approvals::preset('payout');   // WorkflowPreset
```

open() returns the opened ApprovalRequest. The request records the preset name in its workflow column, and a preset expiry lapses the request like expiresIn().

## Preset keys

| Key | Scope | Notes |
| --- | --- | --- |
| `rule` | flat, per stage | An ApprovalRule case or its value; defaults to unanimous when not set (absent, null or blank). |
| `quorum` | flat, per stage | Optional integer threshold; blank is not set. |
| `required_approvers` | flat, per stage | The approvals needed — optional on a flat preset (defaults to the list size), a required positive integer on every stage. The approver list must name at least that many. |
| `expiry` | preset | Optional positive seconds, counted from when the request opens — its one expires\_at, shared by every stage of a staged request; it lapses like expiresIn(). A deadline set when opening replaces it; blank is not set (no expiry). |
| `reject_on_stage_rejection` | staged | A boolean or a boolean spelling ('false', 0, 'off', … read as false); defaults to true — a blank value is not set, so true too. |
| `stages` | staged | Non-empty list of stage shapes: rule, required\_approvers, optional quorum and name (a string when set; blank is not set). |

## Settings the preset defines

A preset defines the round’s rule, quorum, stages and stage rejection, and takes its approvers in open(). So workflow() throws InvalidApprovalRequestException when from(), rule(), any(), quorum(), weighted(), stages() or continueOnRejection() was called on the request builder first — even with a default value. The message names each one, and nothing is opened:

```php
// Refused: the preset would ignore from(), so none of them would be a named approver.
Approvals::request($payout)->from([$cfo, $controller, $treasurer])->workflow('payout')->open();
// InvalidApprovalRequestException: The approval workflow preset [payout] defines the rule, quorum,
// stages and stage rejection itself and takes its approvers in open(), so [from()] cannot be set
// before workflow(). Pass the approvers to open($approvers) instead of from().

// Supported: name the approvers in open().
Approvals::request($payout)->workflow('payout')->open([$cfo, $controller, $treasurer]);
```

Before 1.2.1, workflow() dropped these settings silently: a round opened with ->from(\[...\])->workflow('payout')->open() had no named approvers, so under a preset rule that needs no names (any, a quorum or a required\_approvers) any approver could decide it. Rounds opened that way before you upgraded still have no named approvers — review any that are open.

## A round’s own deadline

Since 1.2.0 a round opened from a preset can take its own deadline. Set it on the workflow builder, or on the request builder before workflow() — the expiry is the one request-builder setting that carries over:

```php
// On the workflow builder.
Approvals::request($payout)->workflow('payout')->expiresIn(3600)->open([$a, $b, $c]);
Approvals::request($release)->workflow('release')->expiringAt(now()->addWeek())->open([[$eng1, $eng2], [$pm]]);

// On the request builder, before workflow(); the expiry carries over.
Approvals::request($payout)->expiresIn(3600)->workflow('payout')->open([$a, $b, $c]);
```

- The caller’s deadline wins — it replaces the preset’s expiry for this round only; the preset itself is unchanged. Set more than once, before or after workflow(), the last one counts.
- Without one, nothing changes — the round gets the preset’s expiry, counted from when it opens, or no deadline if the preset has none.
- One deadline per round, staged or not — it is the request’s expires\_at. A stage that opens later shares it rather than restarting it, and once it passes the whole request reads as expired, whichever stage is open.
- Each round is opened separately — opening another round from the same preset takes the preset’s expiry again unless that call sets its own.
- expiresIn($seconds) counts from the moment you call it, as on the request builder. The action takes the deadline as its fourth argument — OpenWorkflowRequestAction::execute($subject, $preset, $approvers, $expiresAt) — where null keeps the preset’s expiry.

## Approver lists

- Flat preset — pass a flat approver list; those are the request’s named approvers. The preset’s required\_approvers is the number of approvals needed when set, otherwise one per approver.
- Staged preset — pass one approver group per stage, in stage order; the number of groups must equal the number of stages. A stage’s required\_approvers is the number of approvals it needs, so its group must name at least that many approvers.
- A list that names fewer approvers than required\_approvers throws InvalidApprovalRequestException when the request opens.
- A flat preset opened with open(\[\]) may be decided by any approver, as long as its rule resolves without names — a required\_approvers, a quorum or the any rule. Otherwise opening throws InvalidApprovalRequestException.

## Validation

An unknown or malformed preset throws UnknownWorkflowException — a missing name, a preset that is not an array, an empty stages list, a stage without a positive integer required\_approvers, a non-integer quorum, required\_approvers or expiry, an expiry below 1, a non-string stage name, a reject\_on\_stage\_rejection that is neither a boolean nor a boolean spelling, an unknown rule, or a staged preset opened with the wrong number of approver groups. An optional preset key left blank ('') is not set and takes its default: unanimous for rule, none for quorum, expiry and name, true for reject\_on\_stage\_rejection. A workflows value that is not an array throws InvalidConfigurationException instead.

## 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/approvals-for-laravel.md)
