NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
$batch->canRollback();            // completed / failed / cancelled, before undo_until, no rollback yet (or a failed one)
$batch->rollback();
$batch->rollbackReport();         // RollbackReport (null while a queued rollback runs)

Within imports.undo.window (P7D) and after authorize(Rollback), ledger rows marked created and not yet rolled back are processed — inline up to preview.sync_threshold, otherwise as a job batch — each record in its own transaction:

  • The record is loaded through the owner scope, soft-deleted included, with a row lock — an edit between the check and the delete waits and is then seen. Gone or trashed → missing.
  • No checksum stored → kept_changed with reason no_checksum: nothing proves it unchanged. That happens when the commit could not read the created record back through the owner query (ownership() and create() disagree) — the commit reports it.
  • Changed since the import → kept_changed: by checksum (default — a fresh read of the raw attributes minus key and timestamps, so a bare touch() is not a change but a quiet mass update is) or by updated_at (ChangeDetection::updatedAt()).
  • Otherwise your rollback() hook: Deleted (the default), Handled or Kept.
  • A delete that throws (a foreign key in your schema) → failed with the reason; the rest carry on.

Updates are never reverted and linked records are never touched. The ledger records each outcome in the same transaction, so a retried rollback does nothing twice and a second rollback is a no-op. A rollback that ended rollback_failed fires ImportRollbackFailed and can be started again while the window is open — rows already undone are skipped.

Each undo chunk counts as progress, so imports:sweep never gives up on a long undo that is still moving. The undo’s job batch allows failures: one failed chunk does not end the undo while its other chunks still run — it ends rollback_failed (chunk_failed) when the batch finishes.

Outcomes

RollbackOutcomeMeaning
deletedUnchanged since the import and deleted — the default rollback() hook.
handledYour rollback() hook undid it another way (an invitation revoked).
keptYour rollback() hook kept it.
kept_changedChanged since the import — by checksum or updated_at — or no checksum stored (reason no_checksum): kept and listed.
missingAlready gone or trashed.
failedThe delete threw (a host foreign key) — the reason is stored, the rest carry on.

The rollback report

$report = $batch->rollbackReport();

$report->deleted; $report->handled; $report->keptChanged; $report->keptByHook;
$report->linked; $report->missing; $report->failed;
$report->keptTotal();
foreach ($report->kept as $view) {   // ImportedRecordView
    $view->rowNumber; $view->record; $view->action; $view->rollbackOutcome; $view->rollbackReason;
}

Your own rollback

Override rollback() on the importer to undo a record another way — revoke instead of delete:

use RoundlyConsulting\Imports\Enums\RollbackOutcome;

public function rollback(Model $record, ImportContext $context): RollbackOutcome
{
    if ($record->accepted_at !== null) {
        return RollbackOutcome::Kept;      // leave it alone
    }

    $record->revoke();                     // undone another way

    return RollbackOutcome::Handled;       // the default deletes it: RollbackOutcome::Deleted
}

Change detection

A model observer that edits a record right after the import makes it look changed. Exclude such attributes in checksumAttributes(), list the compared attributes with ChangeDetection::checksum([...]), or switch to ChangeDetection::updatedAt():

public function checksumAttributes(Model $record): array
{
    // an observer stamps last_synced_at right after the import — ignore it
    return Arr::except($record->getAttributes(), ['id', 'created_at', 'updated_at', 'last_synced_at']);
}

Past the window, rollback() throws UndoWindowClosedException (409).

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.