---
title: "Rollbacks and history — Lifecycle for Laravel | Roundly"
description: "Undo the last transition or roll back to a history point — all or nothing, with windows, compensation and snapshots — plus the append-only history."
url: https://roundly-consulting.com/open-source/docs/lifecycle-for-laravel/rollbacks-and-history
language: en
---

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

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

# Rollbacks and history

```php
Lifecycles::for($listing)->by($user)->rollback();                 // undo the last transition
Lifecycles::for($listing)->rollbackTo($record);                   // undo everything after a history row (or its id)
Lifecycles::for($listing)->canRollback();                         // Decision
Lifecycles::for($listing)->canRollbackTo($record);                // Decision
Lifecycles::for($listing)->rollback(force: true);                 // ignore snapshot conflicts
```

A rollback reverts rows of the effective history, newest first. It is all or nothing: one refused row refuses the whole call (RollbackDeniedException). A row can be rolled back when all of these hold:

- It is a transition or scheduled row (an expiry is undone with a forward transition such as reactivate).
- Its transition still exists and is not irreversible().
- Its handler is null, implements CompensatesTransition, or is marked reversible(withoutCompensation: true).
- Its window has not passed (reversible(within:), else rollback.default\_window).
- The subject is still in the state that row produced.
- It is not frozen (unless every reverted transition ignoresFreeze()) or sealed.
- The actor passes rollbackRequires() / rollbackGuard() — or the transition’s own actor rules (a systemOnly() transition needs system context), so nobody can undo what they could not have done.
- The reason fits history.reason\_max\_length.
- No snapshotted attribute changed since (unless force).
- The restored state’s quota allows it, counted in the partition the rollback restores.
- The subject is not soft-deleted: canRollback() answers subject\_trashed and rollback() throws SubjectTrashedException.

A rollback restores the state, its entry time, the counters, snapshot attributes and stamp columns. It cancels the schedules the row created and re-opens the schedules it cancelled. It appends rollback rows; a rollback itself cannot be rolled back, and neither can initial or adopted rows. State hooks do not run and rate-limit hits are not refunded. After commit, LifecycleTransitioned (kind rollback) fires per rollback row and LifecycleRolledBack once per call. One rollbackTo() reverts at most rollback.max\_steps rows.

## History

History is append-only: updating or deleting a row through Eloquent throws HistoryIsAppendOnlyException, and only lifecycle:prune (or the force-delete purge) removes rows. Each row records the actor, reason, validated context (minus sensitive keys), snapshot, version and time:

| Kind | Written by | Reversible |
| --- | --- | --- |
| `initial` | the created hook, or initializing a NULL state | no |
| `transition` | apply() / transitionTo() | yes |
| `scheduled` | the sweep running a schedule() | yes |
| `expiry` | the sweep running an expiry | no — undo it with a forward transition such as reactivate |
| `rollback` | a rollback step | no (no redo) |
| `adopted` | drift adoption | no |

```php
Lifecycles::for($listing)->history(20);        // Collection<TransitionRecord>, newest first (default 50)
Lifecycles::for($listing)->lastTransition();   // ?TransitionRecord
$listing->lifecycleHistory()->get();           // the raw LifecycleTransition models
```

TransitionRecord exposes id, lifecycle, kind (TransitionKind), transition, from, to, actorType, actorId, system, reason, context, snapshot, version, revertsId, scheduleId, occurredAt and reverted.

## Pruning

```php
use RoundlyConsulting\Lifecycle\DataTransferObjects\PruneOptions;

Lifecycles::prune(new PruneOptions(historyOlderThanDays: 365, schedulesOlderThanDays: 30));   // PruneResult
```

```bash
php artisan lifecycle:prune                                   # defaults from config
php artisan lifecycle:prune --history-days=365 --schedule-days=30 --dry-run
php artisan lifecycle:prune --database=tenant                 # another connection's package tables
```

Pruning never resets limits or cooldowns — they live on the state record — but it drops rollback points and idempotency keys older than the cutoff. It keeps the mark a neverExpire() call leaves while that stay lasts. --database prunes another connection’s package tables.

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