Brain
Brain: A process-driven architecture alternative for your Laravel Application.
[!WARNING] v2 support ends on June 1, 2026. After that date, only v3 will receive bug fixes and new features. v3 renames the core concepts (
Process→Workflow,Task→Action) — see the Deprecation Map below to migrate.
Brain is an elegant Laravel Package that helps you organize your Laravel application using Domain-Driven Design principles through a simple command-line interface.
#Deprecation Map
The following v2 names are deprecated and will be removed when v2 support ends on June 1, 2026. Update your code to the v3 equivalents before that date.
| v2 (deprecated) | v3 (current) | Notes |
|---|---|---|
Brain\Process |
Brain\Workflow |
Base class for orchestrating a sequence of steps |
Brain\Task |
Brain\Action |
Base class for a single unit of work |
MyProcess::dispatchSync($payload) |
MyWorkflow::run($payload) |
Synchronous invocation |
MyTask::dispatchSync($payload) |
MyAction::run($payload) |
Synchronous invocation |
protected array $tasks = [...] |
protected array $actions = [...] |
List of steps inside a Workflow |
$this->cancelProcess() |
$this->cancelWorkflow() |
Cancel from inside an Action |
php artisan make:process |
php artisan make:workflow |
Generator command |
php artisan make:task |
php artisan make:action |
Generator command |
Brain\Processes\Events\* |
Brain\Workflows\Events\* |
Lifecycle events (Processing, Processed, Error) |
Brain\Tasks\Events\* |
Brain\Actions\Events\* |
Lifecycle events (Processing, Processed, Cancelled, Skipped, Error) |
BRAIN_PROCESS_SUFFIX |
BRAIN_WORKFLOW_SUFFIX |
Suffix env var |
BRAIN_TASK_SUFFIX |
BRAIN_ACTION_SUFFIX |
Suffix env var |
suffixes.process |
suffixes.workflow |
Config key in config/brain.php |
suffixes.task |
suffixes.action |
Config key in config/brain.php |
The v2 classes (Process, Task) remain as aliases until June 1, 2026 so existing projects keep working during the migration window.
#Features
- 🎯 Domain-Driven Structure: Easily create new domains with proper architecture (optional)
- 🔄 Process Management: Generate process classes for complex business operations
- 🔍 Query Objects: Create dedicated query classes for database operations
- ⚡ Task Management: Generate task classes for background jobs and queue operations
- 🔧 Flexible Configuration: Customize root directory, domain organization, and class naming conventions
- 📊 Built-in Logging: Track process and task execution with comprehensive event system
#Gains
- ♻️ Code Reusability: By using tasks, you can easily reuse code across different processes, reducing duplication and enhancing maintainability.
- 🧩 Clear Domain Understanding: The structured approach provides a better understanding of each domain's processes, making it easier to manage and scale your application.
- 🔧 Improved Maintainability: With well-defined domains and processes, maintaining and updating your application becomes more straightforward and less error-prone.
#Installation
You can install the package via composer:
composer require r2luna/brain
#Configuration
Brain can be customized through environment variables or by publishing the configuration file:
php artisan vendor:publish --provider="Brain\BrainServiceProvider"
#Available Configuration Options
#Root Directory
# .env
BRAIN_ROOT=Brain
Defines the main directory where processes, tasks, and queries are created. Default: Brain
#Use Domains
# .env
BRAIN_USE_DOMAINS=false
Organizes code into domain-specific subdirectories. When enabled, creates structure like app/Brain/Users/Processes. Default: false
#Use Suffix
# .env
BRAIN_USE_SUFFIX=false
Automatically appends suffixes to class names (e.g., CreateUser becomes CreateUserProcess). Default: false
#Custom Suffixes
# .env
BRAIN_TASK_SUFFIX=Task
BRAIN_PROCESS_SUFFIX=Process
BRAIN_QUERY_SUFFIX=Query
Customize the suffixes used when BRAIN_USE_SUFFIX=true. Defaults: Task, Process, Query
#Logging
# .env
BRAIN_LOG_ENABLED=false
Enables logging for all processes and tasks. See Logging section for details. Default: false
#Usage
#Creating a Process
php artisan make:process
... follow prompt
name: CreateUserProcess
domain: Users # Only asked if BRAIN_USE_DOMAINS=true
This will create a new process class:
- With domains:
app/Brain/Users/Processes/CreateUserProcess.php - Without domains:
app/Brain/Processes/CreateUserProcess.php
[!IMPORTANT] Note that every task running inside a process executes within a database transaction by default.
#Creating a Task
php artisan make:task
... follow prompt
name: SendWelcomeEmailTask
domain: Users # Only asked if BRAIN_USE_DOMAINS=true
This will create a new task class:
- With domains:
app/Brain/Users/Tasks/SendWelcomeEmailTask.php - Without domains:
app/Brain/Tasks/SendWelcomeEmailTask.php
#Queuable Tasks
To send the task to the queue simply implements Laravel Contract ShouldQueue to the class
<?php namespace App\Brain\User; use Brain\Task; use Illuminate\Contracts\Queue\ShouldQueue; class SendWelcomeNotifications extends Task implements ShouldQueue { public function handle(): self { // return $this; } }
#Delay Queueable Tasks
Brain Tasks has a protected function called runIn() that you can use to determine when do you want to run the job if you need to delay.
class SendWelcomeNotifications extends Task implements ShouldQueue { protected function runIn(): int|Carbon|null { return now()->addDays(2); } ... }
#Setting a Specific Queue with #[OnQueue]
[!WARNING] Do not declare
public string $queue = 'my-queue'on a Task — this causes a PHP fatal error because Laravel'sQueueabletrait already declares$queuewithout a type hint.
Use the #[OnQueue('queue-name')] attribute to assign a specific queue to a Task or Process:
use Brain\Attributes\OnQueue; use Brain\Task; use Illuminate\Contracts\Queue\ShouldQueue; #[OnQueue('emails')] class SendWelcomeNotifications extends Task implements ShouldQueue { public function handle(): self { // This task will run on the "emails" queue return $this; } }
When applied to a Process, the attribute does two things:
- The Process itself is dispatched to that queue (if it implements
ShouldQueue) - All queued child tasks inherit the Process queue — unless the task defines its own
#[OnQueue]
use Brain\Attributes\OnQueue; use Brain\Process; use Illuminate\Contracts\Queue\ShouldQueue; #[OnQueue('strava')] class SyncActivitiesProcess extends Process implements ShouldQueue { protected array $tasks = [ FetchActivities::class, // ShouldQueue → runs on "strava" (inherited) SaveActivities::class, // sync task → unaffected NotifyUser::class, // has #[OnQueue('emails')] → runs on "emails" (own queue wins) ]; }
#Queue Execution Flows
Below are the three most common queue configurations.
1. Process + all Tasks on the same queue
The process and every queued task run on strava:
flowchart LR
D((Dispatch)) --> Q[strava queue]
subgraph Q[strava queue]
direction LR
P[SyncActivitiesProcess] --> T1[FetchActivities]
T1 --> T2[TransformData]
T2 --> T3[SaveActivities]
end
style Q fill:#1a1a2e,stroke:#e94560,color:#eee
style P fill:#0f3460,stroke:#e94560,color:#eee
style T1 fill:#16213e,stroke:#0f3460,color:#eee
style T2 fill:#16213e,stroke:#0f3460,color:#eee
style T3 fill:#16213e,stroke:#0f3460,color:#eee
#[OnQueue('strava')] class SyncActivitiesProcess extends Process implements ShouldQueue { protected array $tasks = [ FetchActivities::class, // implements ShouldQueue TransformData::class, // implements ShouldQueue SaveActivities::class, // implements ShouldQueue ]; }
2. Process on a queue, Tasks run synchronously inside it
The process is queued, but internally its tasks run one after another in the same job:
flowchart LR
D((Dispatch)) --> Q
subgraph Q[strava queue]
subgraph P[SyncActivitiesProcess]
direction LR
T1[FetchActivities] --> T2[TransformData] --> T3[SaveActivities]
end
end
style Q fill:#1a1a2e,stroke:#e94560,color:#eee
style P fill:#0f3460,stroke:#e94560,color:#eee
style T1 fill:#16213e,stroke:#0f3460,color:#eee
style T2 fill:#16213e,stroke:#0f3460,color:#eee
style T3 fill:#16213e,stroke:#0f3460,color:#eee
#[OnQueue('strava')] class SyncActivitiesProcess extends Process implements ShouldQueue { protected array $tasks = [ FetchActivities::class, // sync (no ShouldQueue) TransformData::class, // sync SaveActivities::class, // sync ]; }
3. Process on a queue, mixed sync and queued Tasks
The process is queued. Some tasks run synchronously inside the process job, others are dispatched to their own queue jobs:
flowchart LR
D((Dispatch)) --> Q
subgraph Q[strava queue]
subgraph P[SyncActivitiesProcess]
direction LR
T1[FetchActivities\nsync] --> T2[TransformData\nsync]
end
T2 --> T3[SaveActivities\nqueued]
T3 --> T4[NotifyUser\nqueued]
end
T4 -.-> E[emails queue]
subgraph E[emails queue]
T4real[NotifyUser]
end
style Q fill:#1a1a2e,stroke:#e94560,color:#eee
style E fill:#1a1a2e,stroke:#53a653,color:#eee
style P fill:#0f3460,stroke:#e94560,color:#eee
style T1 fill:#16213e,stroke:#0f3460,color:#eee
style T2 fill:#16213e,stroke:#0f3460,color:#eee
style T3 fill:#16213e,stroke:#0f3460,color:#eee
style T4 fill:#16213e,stroke:#0f3460,color:#eee
style T4real fill:#16213e,stroke:#53a653,color:#eee
#[OnQueue('strava')] class SyncActivitiesProcess extends Process implements ShouldQueue { protected array $tasks = [ FetchActivities::class, // sync — runs inside the process job TransformData::class, // sync — runs inside the process job SaveActivities::class, // implements ShouldQueue → dispatched to "strava" NotifyUser::class, // implements ShouldQueue + #[OnQueue('emails')] → dispatched to "emails" ]; }
[!TIP] A task's own
#[OnQueue]always takes precedence over the Process-level queue.
#Conditional Run Tasks in a Process
You can use a protected function runIf() to conditionally run a task.
/** * @property-read int $amount */ class SendWelcomeNotifications extends Task { protected function runIf(): bool { return $this->amount > 200; } ... }
#Validating Task Properties
You can validate the properties passed to a task by defining a rules() method that returns an array of validation rules.
/** * @property-read User $user * @property string $message */ class SendWelcomeNotifications extends Task { public function rules(): array { return [ 'user' => 'required', 'message' => 'required|string|max:255', ]; } public function handle(): self { // ... return $this; } ... }
Rules will validate based on the payload that was passed to the task when it was dispatched, and will override the default validation based on the docblock @property-read annotations.
#Task helper methods
toArray(): Returns the task properties as an array. Ex.['user' => $this->user]
#Cancel the Process
If you need, for any reason, cancel the process from inside a task. You can call cancelProcess() method to do it.
class AddRoles extends Task { public function handle(): self { if($anyReason) { $this->cancelProcess(); } return $this; } }
[!CAUTION] This will not work if the task is setup to run in a queue.
#Sensitive Properties with #[Sensitive]
Use the #[Sensitive('key1', 'key2')] attribute to mark payload properties that should be automatically redacted in logs, JSON serialization, and debug output. Sensitive values are wrapped in SensitiveValue — they remain accessible inside your task via $this->key, but are replaced with ********** everywhere else.
use Brain\Attributes\Sensitive; use Brain\Task; /** * @property-read string $email * @property string $password * @property string $credit_card */ #[Sensitive('password', 'credit_card')] class CreateUser extends Task { public function handle(): self { // $this->password returns the real value // but logs, JSON, and debug output show "**********" return $this; } }
Process-level inheritance: When #[Sensitive] is applied to a Process, all child tasks automatically inherit the sensitive keys — even if the tasks themselves don't declare the attribute. Task-level and process-level keys are merged and deduplicated.
use Brain\Attributes\Sensitive; use Brain\Process; #[Sensitive('password', 'credit_card')] class CreateUserProcess extends Process { protected array $tasks = [ ValidateInput::class, // password & credit_card are sensitive here ChargeCustomer::class, // password & credit_card are sensitive here too SendConfirmation::class, ]; }
[!TIP] The
brain:show -vvcommand displays a[sensitive]indicator next to sensitive properties.
#Creating a Query
php artisan make:query
... follow prompt
name: GetUserByEmailQuery
domain: Users # Only asked if BRAIN_USE_DOMAINS=true
model: User
This will create a new query class:
- With domains:
app/Brain/Users/Queries/GetUserByEmailQuery.php - Without domains:
app/Brain/Queries/GetUserByEmailQuery.php
#Example Usage
// Using a Query $user = GetUserByEmailQuery::run('john@example.com'); // Setting up a Process class CreateUserProcess extends Process { protected array $tasks = [ RegisterUserTask::class, SendWelcomeEmailTask::class, // Async task NotifyStaffTask::class, // Async task SubProcess::class ]; } // Running a Process synchronously CreateUserProcess::run([ 'name' => 'John Doe', 'email' => 'john@example.com' ]); // Running a Task without a process SendWelcomeEmailTask::run([ 'user' => $user ]); // Dispatching to the queue CreateUserProcess::dispatch([ 'name' => 'John Doe', 'email' => 'john@example.com' ]);
#Visualizing Your Brain
Use the brain:show command to visualize your entire Brain structure in the terminal:
php artisan brain:show
USERS ├── PROC CreateUserProcess ······························· ├── TASK SendWelcomeEmailTask ···························· ├── TASK NotifyStaffTask ································· queued └── QERY GetUserByEmailQuery ····························· PAYMENTS ├── PROC PaymentSucceededProcess ················· chained └── QERY GetPaymentQuery ································
#Filtering by type
Use flags to show only specific types. Flags can be combined:
php artisan brain:show -p # only processes php artisan brain:show -t # only tasks php artisan brain:show -Q # only queries php artisan brain:show -p -t # processes and tasks
#Filtering by name
Use --filter to search by class name (case-insensitive):
php artisan brain:show --filter=User # anything with "User" in the name php artisan brain:show -p --filter=Payment # processes matching "Payment"
When combining -p with --filter, if the filter matches a sub-task name, the parent process is shown with only the matching sub-tasks. If it matches a process name, all its sub-tasks are shown automatically.
#Verbosity levels
php artisan brain:show -v # show sub-tasks inside processes php artisan brain:show -vv # also show task properties (input/output)
USERS ├── PROC PaymentSucceededProcess ················· chained │ ├── 1. T SavePaymentTask ················ queued │ └── 2. T InviteUserTask ························· ├── TASK CreateCommentTask ······························· │ → user_id: int │ ← comment: \Comment|null └── QERY ExampleQuery ···································
→input property (@property-read)←output property (@property)
#Running Processes & Tasks
Use the brain:run command to interactively select and execute a Process or Task directly from the terminal:
php artisan brain:run
The command guides you through an interactive flow:
- Select a target — Search and pick any Process or Task in your Brain directory
- Choose dispatch mode — Sync (
dispatchSync) or Async (dispatch) - Fill payload — Enter values for required properties, optionally fill optional ones
- Preview — Review the dispatch call and payload before executing
- Execute — Run the target and see the resulting payload
#Rerunning a previous execution
Every successful run is saved to history (storage/brain/run-history.json, max 50 entries). Use --rerun to replay a previous execution:
php artisan brain:run --rerun
This lets you search through past runs, preview the saved payload, and re-execute with the same parameters.
#Architecture
Brain helps you organize your code into three main concepts:
- Processes: Complex business operations that might involve multiple steps
- Queries: Database queries and data retrieval operations
- Tasks: Sync/Async operations that can be called as part of a process or not
Each concept is organized within its respective domain, promoting clean architecture and separation of concerns.
#Logging
Brain provides built-in logging functionality to track the execution and outcomes of processes and tasks. By default, logging is disabled but can be enabled through configuration.
#Configuration
To enable logging, set the BRAIN_LOG_ENABLED environment variable or update the config file:
# .env
BRAIN_LOG_ENABLED=true
Or publish and modify the configuration file:
php artisan vendor:publish --provider="Brain\BrainServiceProvider"
Then update config/brain.php:
'log' => env('BRAIN_LOG_ENABLED', true),
#Events
Brain dispatches events throughout the lifecycle of processes and tasks. These events can be used for logging, monitoring, or triggering additional actions.
#Process Events
Brain\Processes\Events\Processing- Dispatched when a process starts executingBrain\Processes\Events\Processed- Dispatched when a process completes successfullyBrain\Processes\Events\Error- Dispatched when a process encounters an error
Each event contains:
process(string): The process class namerunProcessId(string): A unique ID for this executionpayload(array|object): The data passed to the processmeta(array): Additional metadata
#Task Events
Brain\Tasks\Events\Processing- Dispatched when a task starts executingBrain\Tasks\Events\Processed- Dispatched when a task completes successfullyBrain\Tasks\Events\Cancelled- Dispatched when a task is cancelled viacancelProcess()Brain\Tasks\Events\Skipped- Dispatched when a task is skipped (whenrunIf()returns false)Brain\Tasks\Events\Error- Dispatched when a task encounters an error
Each event contains:
task(string): The task class namepayload(array|object|null): The data passed to the taskprocess(string|null): The process class name (if running within a process)runProcessId(string|null): The process execution ID (if running within a process)meta(array): Additional metadata
#Custom Event Listeners
You can create custom event listeners to handle these events:
// app/Providers/EventServiceProvider.php use Brain\Processes\Events\Processed as ProcessProcessed; use Brain\Tasks\Events\Error as TaskError; protected $listen = [ ProcessProcessed::class => [ NotifyAdminOfProcessCompletion::class, ], TaskError::class => [ LogTaskErrorToExternalService::class, ], ];
#Upgrading to v2.0
Version 2.0 introduces new configuration options and enhanced flexibility for organizing your Brain components.
#Breaking Changes
#Configuration File Structure
The configuration file has been completely restructured with new options. If you've published the config file in v1.x, you'll need to republish it:
php artisan vendor:publish --provider="Brain\BrainServiceProvider" --force
Or manually update your config/brain.php to match the new structure (see Configuration section).
#Default Behavior Changes
Domain Organization: In v1.x, Brain automatically organized components into domain subdirectories. In v2.0, this is now opt-in via the use_domains configuration:
// v1.x behavior (automatic domains) app/Brain/Users/Processes/CreateUserProcess.php // v2.0 default behavior (flat structure) app/Brain/Processes/CreateUserProcess.php // v2.0 with BRAIN_USE_DOMAINS=true (same as v1.x) app/Brain/Users/Processes/CreateUserProcess.php
#Migration Guide
To maintain v1.x behavior in v2.0, add this to your .env:
BRAIN_USE_DOMAINS=true
This will ensure your existing code structure remains unchanged.
#New Features in v2.0
#Flexible Root Directory
You can now customize where Brain components are created:
# .env BRAIN_ROOT=Domain # Creates files in app/Domain instead of app/Brain
#Optional Class Suffixes
Enable automatic suffixing for cleaner class names:
# .env BRAIN_USE_SUFFIX=true # Now you can write: php artisan make:process CreateUser # Creates: CreateUserProcess.php
#Enhanced Logging
Built-in logging system for tracking process and task execution. See Logging section for details.
#Testing
composer test
#Contributing
Please see CONTRIBUTING for details.
#Security
If you discover any security-related issues, please email rafael@lunardelli.me instead of using the issue tracker.
#Credits
#License
The MIT License (MIT). Please see License File for more information.