---
title: "Strict writes and drift — Lifecycle for Laravel | Roundly"
description: "Why direct status writes throw, which write paths bypass the engine, and how allowDirectWrites() and adoption reconcile drift."
url: https://roundly-consulting.com/open-source/docs/lifecycle-for-laravel/strict-writes
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)

# Strict writes and drift

The model attribute is the source of truth. With strict\_writes on, saving a changed lifecycle attribute throws DirectStateWriteException:

| Write path | Covered | Outcome |
| --- | --- | --- |
| `$m->status = …; $m->save(), update([...]), forceFill([...])->save(), push(), increment('x', 1, ['status' => …])` | yes | throws (or adopted inside allowDirectWrites) |
| `Model::create([...]), factories ->create() / ->state([...])` | creation | any declared state accepted; initial history row |
| `factory afterCreating / seeder changing a saved model’s state` | yes | throws: wrap in Lifecycles::allowDirectWrites() or seed with LIFECYCLE\_STRICT\_WRITES=false |
| `saveQuietly(), withoutEvents(), Model::query()->update(), relation update(), upsert(), insert(), DB::table(), raw SQL` | no (no model event) | drift: adopted at the next mutation, or by lifecycle:adopt |

```php
Lifecycles::allowDirectWrites(fn () => $listing->update(['status' => ListingStatus::Closed]));  // adopted at once

Lifecycles::for($listing)->adopt();                      // reconcile one subject now
Lifecycles::model(Listing::class)->adopt(chunk: 500);    // every row (= php artisan lifecycle:adopt)
```

Adoption writes an adopted history row, cancels the old state’s schedules, schedules the new state’s expiry and fires LifecycleAdopted and LifecycleTransitioned. The time in the adopted state starts at the moment of adoption, because the real entry time is unknown. A NULL stored state (rows from insert(), or a column added to an existing table) is set to the initial state. An adopted row is not reversible, so nothing below it can be undone.

With strict\_writes off, a model save that changes the state is adopted immediately — history row, schedules and events. Only writes that bypass model events leave drift behind.

## Evolving a definition

History stores raw state values and transition names. Renaming a transition makes its past rows irreversible. Renaming or removing a state needs a data migration — wrapped in Lifecycles::allowDirectWrites(), or followed by php artisan lifecycle:adopt. Run php artisan lifecycle:validate after every change.

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