---
title: "Actions & DTOs — Approvals for Laravel | Roundly"
description: "Every action class with its execute() signature, plus the DecisionData and ApprovalRequestData DTOs the actions take."
url: https://roundly-consulting.com/open-source/docs/approvals-for-laravel/actions-and-dtos
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)

# Actions & DTOs

The actions in RoundlyConsulting\\Approvals\\Actions are the code the facade and the manager run — single-purpose classes with execute() for composing into your own actions, jobs and commands. The facade-to-action map is under DI and actions.

## Action signatures

```php
use RoundlyConsulting\Approvals\Actions\ApproveAction;
use RoundlyConsulting\Approvals\Actions\CancelApprovalAction;
use RoundlyConsulting\Approvals\Actions\CloseApprovalRequestAction;
use RoundlyConsulting\Approvals\Actions\ExpireApprovalsAction;
use RoundlyConsulting\Approvals\Actions\RejectAction;
use RoundlyConsulting\Approvals\Actions\RequestApprovalAction;
use RoundlyConsulting\Approvals\Actions\ToggleApprovalAction;
use RoundlyConsulting\Approvals\DataTransferObjects\DecisionData;
use RoundlyConsulting\Approvals\Enums\ApprovalStatus;

app(ApproveAction::class)->execute($user, $post, DecisionData::approved('Looks good'));
app(RejectAction::class)->execute($user, $post, DecisionData::rejected('Needs work'));
app(RequestApprovalAction::class)->execute($user, $post);                    // pending Approval
app(RequestApprovalAction::class)->execute($user, $post, new DecisionData(ApprovalStatus::Pending, 'Check the sources', now()->addDay()));
app(CancelApprovalAction::class)->execute($user, $post, 'Changed my mind');   // ?Approval
app(ToggleApprovalAction::class)->execute($user, $post);                     // bool
app(ToggleApprovalAction::class)->execute($lead, $release, DecisionData::approved('LGTM'), $request);
app(CloseApprovalRequestAction::class)->execute($purchase);                  // int — rounds closed, as cancelled
app(CloseApprovalRequestAction::class)->execute($purchase, ApprovalStatus::Expired, $round); // only $round
app(ExpireApprovalsAction::class)->execute();                                // int
app(ExpireApprovalsAction::class)->execute(subjectType: Post::class);        // int — posts only
```

| Action | execute() | Notes |
| --- | --- | --- |
| `ApproveAction` | ($actor, $approvable, ?DecisionData, ?ApprovalRequest): Approval | Gate check, named approvers, delegation, weight and stage; supersedes a held rejection; a repeat is a no-op; resolves the request. |
| `RejectAction` | ($actor, $approvable, ?DecisionData, ?ApprovalRequest): Approval | Gate check, named approvers, delegation, weight and stage; supersedes a held approval; reads no expiry — a rejection has none; resolves the request. |
| `RequestApprovalAction` | ($actor, $approvable, ?DecisionData, ?ApprovalRequest): Approval | Records a pending decision with the DecisionData reason and expiry (its reply-by deadline) — or returns the actor’s live one unchanged; fires ApprovalRequested. It ignores the DTO’s status and weight: an ask always carries weight 1, silently — only the facade refuses weight(). |
| `CancelApprovalAction` | ($actor, $approvable, ?string $reason, ?ApprovalRequest): ?Approval | Withdraws the actor’s live decision, or one it made as a delegate while the delegation is still in force; on a staged request only in the open stage. $reason replaces the decision’s; null keeps it. |
| `ToggleApprovalAction` | ($actor, $approvable, ?DecisionData, ?ApprovalRequest): bool | Approves through ApproveAction with the DecisionData and request, or withdraws and soft-deletes the live approval with the DTO’s reason, firing ApprovalCancelled like CancelApprovalAction. The live approval decides the direction, not the DTO’s status. |
| `OpenApprovalRequestAction` | (ApprovalRequestData $data): ApprovalRequest | Opens a flat request and stores its named approvers; required\_approvers defaults to one per named approver. |
| `RequestStagedApprovalAction` | ($subject, $stages, $rejectOnStageRejection = true, ?$expiresAt, ?$workflow): ApprovalRequest | Opens a staged pipeline; each stage stores its named approvers. |
| `OpenWorkflowRequestAction` | ($subject, WorkflowPreset, $approvers, ?$expiresAt): ApprovalRequest | Opens a request from a resolved preset; $expiresAt replaces the preset’s expiry for this round, null keeps it. |
| `DelegateApprovalsAction` | ($delegator, $delegate, ?$startsAt, ?$endsAt): ApprovalDelegation | Validates and saves a delegation; fires ApprovalDelegated. |
| `RevokeApprovalDelegationAction` | ($delegator, ?$delegate): int | Revokes active and scheduled delegations; returns the count. |
| `ExpireApprovalsAction` | (?$now, ?$subjectType): int | Lapses overdue asks, approvals and requests — optionally of one subject type; returns the count. |
| `CloseApprovalRequestAction` | ($subject, ApprovalStatus $outcome = Cancelled, ?ApprovalRequest): int | Closes the subject’s open rounds — or only the given one — as cancelled or expired, with no actor and no gate check; returns how many it closed. |

## Attaching to a specific request

The approve, reject, ask, cancel and toggle actions accept the request to attach to as their fourth argument — the facade passes it with within(). Without it, they use the approvable’s latest open request, or record a standalone decision when the approvable never had one. A request of another subject throws InvalidApprovalRequestException and a closed one ClosedApprovalRequestException, before anything is written. CloseApprovalRequestAction takes the request as its third argument and closes only that round:

```php
use RoundlyConsulting\Approvals\Actions\ApproveAction;
use RoundlyConsulting\Approvals\DataTransferObjects\DecisionData;
use RoundlyConsulting\Approvals\Facades\Approvals;

$request = Approvals::request($release)->from([$lead, $qa])->open();

// The action's fourth argument is what within($request) passes on the facade.
app(ApproveAction::class)->execute($lead, $release, DecisionData::approved('Ship it'), $request);
```

## DecisionData

DecisionData carries the intent of a decision. Build it with a named constructor:

| Constructor | Result |
| --- | --- |
| `DecisionData::approved(?$reason, ?$expiresAt, ?$weight)` | An approval — weight overrides the resolved weight for this one decision. |
| `DecisionData::rejected(?$reason, ?$weight)` | A rejection — weight works as for approved(). |
| `DecisionData::pending(?$expiresAt)` | A pending decision. For an ask with a reason use new DecisionData(ApprovalStatus::Pending, $reason, $expiresAt). |
| `DecisionData::cancelled(?$reason)` | A cancellation. |

RequestApprovalAction writes the DTO’s reason as the ask’s reason and its expiry as the reply-by deadline, but ignores its status and weight: it always records a pending decision with weight 1, and a weight set on the DTO is dropped without an error — only the facade refuses weight() before ask(). Set the weight on the decision that answers the ask.

## Staged requests with a workflow name

RequestStagedApprovalAction accepts an expiry and a workflow name, which it stamps on the request — the facade and the trait set the expiry, the action also takes the name:

```php
use RoundlyConsulting\Approvals\Actions\RequestStagedApprovalAction;
use RoundlyConsulting\Approvals\DataTransferObjects\StageDefinition;
use RoundlyConsulting\Approvals\Enums\ApprovalRule;

app(RequestStagedApprovalAction::class)->execute(
    $release,
    [
        new StageDefinition([$eng1, $eng2], name: 'engineering'),
        new StageDefinition([$product], ApprovalRule::Any, name: 'product'),
    ],
    rejectOnStageRejection: true,
    expiresAt: now()->addDays(3),
);
```

## ApprovalRequestData

ApprovalRequestData is the read-only input of OpenApprovalRequestAction: subject, approvers (the named approvers — none named means any approver may decide), rule, quorum, expiresAt, plus an optional requiredApprovers (defaults to one per named approver) and workflow name.

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