Laravel
I Built a Playbook So My AI Agents Stop Ignoring My Conventions
Technically correct code can still be wrong for your architecture. I packaged Labrodev conventions as a Boost skill so agents follow Actions, Data objects, thin models, and stubs—consistently, every session.
There's a specific kind of frustration that comes from watching a coding agent generate technically correct code that is architecturally wrong for your project.
The logic works. The tests pass. And yet, you spend the next twenty minutes moving things around, renaming classes, extracting logic that doesn't belong in a controller, removing $fillable from a model that should never have had it. The agent didn't break anything. It just didn't know how you work.
That's the problem I set out to solve.
The Gap Between "Works" and "Fits"
Most agents are good at producing code that works. They've been trained on millions of Laravel repositories — and the majority of those repositories do not follow any consistent architectural pattern. They mix business logic into controllers, use $fillable everywhere, define authorization in middleware, put queries in models, and call it done.
That's fine. Most code doesn't need to be more than that.
But when you've spent years developing a specific way of organising Laravel applications — a way that scales, that reads cleanly, that AI can reason about — you don't want your coding agents defaulting to the common denominator. You want them to follow your patterns.
The problem is that agents have no idea what your patterns are unless you tell them. Every time. In every session. Hoping they don't forget mid-task.
So I stopped telling them in conversation and started writing it down in a format they can actually consume: the Labrodev Playbook.
What the Playbook Actually Is
It's a set of architectural conventions, naming rules, boundary definitions, and code templates — packaged as a skill for AI development agents. Not a framework. Not a package that installs service providers or registers bindings. Nothing that runs at runtime. Pure knowledge, structured for machine consumption.
It lives at labrodev/laravel-playbook on GitHub and gets installed into your project via Laravel Boost:
composer require laravel/boost
php artisan boost:install
php artisan boost:add-skill labrodev/laravel-playbook --skill=labrodev-playbook
After that, Cursor, Claude Code, Codex, and Junie all read from it. The same conventions, consistently, across every session.
The Architecture Behind the Playbook
Let me be direct about what these conventions actually are.
Domain-Driven Organisation
The project structure follows a clear split between Core and App:
Core/Domain/{Domain}/ ← business logic lives here
App/Dashboard/{Domain}/ ← delivery layer lives here
Core owns everything that matters: Actions, Models, Data objects, Policies, Observers, Rules, Services. App/Dashboard owns everything that touches HTTP: Controllers, ViewModels, IndexQueries, Exports. The dependency direction only goes one way — Dashboard depends on Core, never the reverse.
Cross-domain interaction goes through events or orchestrators. Domain A does not import Domain B directly.
Actions, Not Fat Controllers
Every write-side operation is a single-responsibility Action class. Not a service with ten methods. Not a controller with business logic buried inside. One action, one responsibility, one public method:
final class ProductCreate
{
public function execute(ProductData $productData): Product
{
$product = new Product();
$product->name = $productData->name;
$product->status = ProductStatus::Draft;
$product->save();
return $product;
}
}
Actions are named entity-first: ProductCreate, OrderCancel, InvoiceGenerate. They expose execute(). No __invoke(). No handle(). No ambiguity.
Controllers inject actions as method arguments and call them. That's it. Controllers handle I/O — they do not make decisions.
No Request Classes. Data Objects Instead.
There are no FormRequest classes in this architecture. Input mapping and validation happen in Spatie Data objects, which are type-hinted directly in controller signatures:
public function __invoke(ProductData $productData, ProductCreate $productCreate): RedirectResponse
{
$productCreate->execute(productData: $productData);
return redirect()->route('products.index');
}
Spatie Data resolves from the request automatically. The controller never touches $request->all(). The domain never sees raw input. The Data class is the contract — explicit, typed, validated.
Relations in Data objects use UUID casters, never internal IDs. public ?Service $service with #[WithCast(ServiceUuidCaster::class)], not public ?int $service_id. Internal IDs don't leave the backend.
Tiny Models
Models are persistence objects. They define relationships, casts, and visibility. That's close to all they do.
No $fillable. Attributes are assigned explicitly in Actions. No query scopes. No business workflows. No cross-entity orchestration.
Every model uses Laravel 13's class-level attributes:
#[ObservedBy(ProductObserver::class)]
#[CollectedBy(ProductCollection::class)]
#[UsePolicy(ProductPolicy::class)]
#[UseFactory(ProductFactory::class)]
final class Product extends BaseModel
No boot() method. No observe() calls. No newCollection() override. The attributes do the wiring — declarative, readable, and correct.
Query Classes for Read Side
Read-side operations don't belong in models, controllers, or actions. They live in dedicated Query classes that return Builder only — never resolved collections, never integers, never arrays. The Dashboard layer calls them and decides how to paginate or present the results.
Spatie QueryBuilder handles filtering, sorting, and pagination at the Dashboard boundary. The domain stays clean.
Stubs as Executable Specifications
Every class type — Action, Data, Model, Observer, Policy, Collection, Service, Job — has a corresponding stub file. These stubs define exactly how a class must look: modifiers, constructor shape, method names, visibility, allowed dependencies.
When an agent generates code, it reads the stub first and uses it as a structural template. Not a suggestion. A specification.
This is the part that actually changes how agents behave. When the stub says final class, the agent writes final class. When the stub says execute(), the agent writes execute(). The stub removes the guess.
How It Wires Into Agents
Laravel Boost installs the playbook as a skill — a structured markdown document with a metadata header that tells the agent when to activate it:
name: labrodev-playbook
description: "Use this skill whenever working on a Labrodev Laravel project..."
Boost then generates or updates CLAUDE.md, AGENTS.md, and .cursor/rules/ with references to the installed skill. Every agent that reads these files — Cursor, Claude Code, Codex, Junie — picks up the conventions automatically.
No more pasting conventions into the chat. No more hoping the agent remembers. The playbook is part of the project, versioned alongside the code, and updated with a single command:
php artisan boost:add-skill labrodev/laravel-playbook --skill=labrodev-playbook --force
Recipes: Beyond Conventions
The playbook also ships with recipes — task-specific execution guides that go beyond architecture.
The first one covers implementing a complete CRUD page from prototype to production. It walks through migrations, domain layer (Resource, Policy, Observer), dashboard layer (IndexQuery, Controllers, ViewModels, Routes), frontend (DataTable, form pages, Wayfinder), Data classes, Actions, code quality checks, and tests. In the right order. With the right checks at each step.
When an agent receives a task to implement a new page, it reads the recipe before touching any file. The recipe is not documentation — it's a checklist the agent executes.
More recipes will follow as the patterns mature.
Why I Made It Public
The conventions I've codified here are not secrets. Most senior Laravel developers have arrived at similar conclusions through similar experience. Domain-driven organisation, thin models, single-responsibility actions, typed input objects — these ideas have been around for years.
What I've done is write them down precisely enough that a machine can follow them. That's the actual contribution.
Making it public means anyone who works the same way can skip the writing and start with the conventions. Fork it, adjust it, extend it. The playbook is designed to be a starting point, not a final word.
If you work with AI coding agents and you've felt the frustration of watching them produce architecturally wrong code — this is the answer I built for myself. It might be useful for you too.
The playbook is at github.com/labrodev/laravel-playbook.
It installs in three commands. The rest is just writing code the way you actually want it written.
Thanks for reading.
Back to Journal