PHPackages                             codenzia/filament-workflow - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. codenzia/filament-workflow

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

codenzia/filament-workflow
==========================

Visual workflow automation engine for Filament 4+ built on filament-diagrammer

00PHP

Since Mar 17Pushed 2w agoCompare

[ Source](https://github.com/Codenzia/filament-workflow)[ Packagist](https://packagist.org/packages/codenzia/filament-workflow)[ RSS](/packages/codenzia-filament-workflow/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependenciesVersions (1)Used By (0)

Filament Workflow
=================

[](#filament-workflow)

Visual workflow automation engine for Filament 4+ built on [filament-diagrammer](https://github.com/Codenzia/filament-diagrammer).

Features
--------

[](#features)

- Visual flow builder with drag-and-drop nodes
- Trigger → Condition → Delay → Action pipeline
- IF/ELSE branching with condition nodes
- Time-based triggers (due date reminders, overdue escalation)
- Extensible trigger and action registry with **dynamic config forms**
- Workflow templates (predefined flows)
- Subclassable designer for app-specific customization
- Execution logging and audit trail
- Global + project-scoped workflows

Requirements
------------

[](#requirements)

- PHP 8.3+
- Laravel 12+
- Filament 4+
- `codenzia/filament-diagrammer`

Installation
------------

[](#installation)

```
composer require codenzia/filament-workflow
```

Publish and run migrations:

```
php artisan vendor:publish --tag=filament-workflow-migrations
php artisan migrate
```

Publish the config file (optional):

```
php artisan vendor:publish --tag=filament-workflow-config
```

### Tailwind v4 Custom Theme

[](#tailwind-v4-custom-theme)

If your Filament panel uses a custom theme (Tailwind CSS v4), add source paths for both this package and its dependency `filament-diagrammer`:

```
/* resources/css/filament/{panel}/theme.css */
@source '../../../../vendor/codenzia/*/src/**/*.php';
@source '../../../../vendor/codenzia/*/resources/views/**/*.blade.php';
```

This wildcard pattern covers all Codenzia packages (including the `filament-diagrammer` dependency) at once.

Then rebuild your assets (`npm run build`).

Quick Start
-----------

[](#quick-start)

### 1. Add `HasWorkflows` trait to your model

[](#1-add-hasworkflows-trait-to-your-model)

```
use Codenzia\FilamentWorkflow\Concerns\HasWorkflows;

class Task extends Model
{
    use HasWorkflows;
}
```

This automatically dispatches workflow evaluation on model `created` and `updated` events.

### 2. Register triggers and actions

[](#2-register-triggers-and-actions)

In your `AppServiceProvider::boot()`:

```
use Codenzia\FilamentWorkflow\Engine\WorkflowEngine;

// Triggers
WorkflowEngine::registerTrigger('task.status_changed', TaskStatusChangedTrigger::class);
WorkflowEngine::registerTrigger('task.assigned', TaskAssignedTrigger::class);

// Actions
WorkflowEngine::registerAction('change_task_status', ChangeTaskStatusAction::class);
WorkflowEngine::registerAction('assign_user', AssignUserAction::class);
WorkflowEngine::registerAction('escalate', EscalateAction::class);

// Model fields (shown in condition, trigger, and action config dropdowns)
WorkflowEngine::registerModelFields(Task::class, [
    'title' => 'Title',
    'status' => 'Status',
    'priority' => 'Priority',
    'assigned_to_user_id' => 'Assigned To',
    'due_date' => 'Due Date',
    'progress' => 'Progress (%)',
    // ...
]);
```

### 3. Embed the designer in a page

[](#3-embed-the-designer-in-a-page)

```
@livewire(
    \Codenzia\FilamentWorkflow\Pages\WorkflowDesigner::class,
    ['projectId' => $project->id, 'modelType' => Task::class]
)
```

The `modelType` parameter is **required** — the designer will abort if not provided.

### Diagram &amp; Rules Tabs

[](#diagram--rules-tabs)

The designer includes two tabs above the canvas:

- **Diagram** — Visual drag-and-drop canvas (default)
- **Rules** — Structured form-based step list

Both tabs edit the same underlying data. Changes made in one tab are immediately visible when switching to the other.

The **Rules tab** shows:

- Connected flows as a vertical step list with condition branching (Yes/No columns)
- Inline `+` buttons between steps to insert new nodes
- An "Add Step" button for creating new trigger/action/condition/delay steps
- An **Unconnected Steps** section (collapsed by default) for orphaned nodes

Double-click any step card to open its settings editor.

When switching from Rules back to Diagram, nodes created in the rules view are automatically positioned using a layered tree layout algorithm.

Configuration
-------------

[](#configuration)

Published to `config/filament-workflow.php`:

```
return [
    // Queue name for automation jobs
    'queue' => 'automations',

    // Max nodes per workflow run (infinite loop prevention)
    'max_nodes_per_run' => 50,

    // Time trigger check interval (minutes)
    'time_trigger_interval' => 15,

    // Hours before same trigger can re-fire on same model
    'dedup_window_hours' => 24,

    // Days to keep execution logs
    'log_retention_days' => 90,
];
```

Concepts
--------

[](#concepts)

### Node Types

[](#node-types)

TypeColorPurposeConnections**Trigger**GreenEntry point — what event starts the flowNo inputs, one output**Condition**YellowFilter with YES/NO branchingOne input, two outputs (Yes/No)**Delay**BlueWait before continuing (minutes/hours/days)One input, one output**Action**PurpleExecute an operationOne input, any outputsEach node type includes a `description()` method with localizable help text (via `filament-workflow::node-types.*`). The palette sidebar shows a `?` tooltip on hover with this description.

Double-click any node to open its settings editor. Right-click for the context menu with Settings, Delete, Connect to, and more.

### Triggers (Built-in)

[](#triggers-built-in)

- **ModelCreated** — fires when a model is created
- **ModelUpdated** — fires when a model is updated
- **FieldChanged** — fires when a specific field changes (optional `from`/`to` constraints)

### Actions (Built-in)

[](#actions-built-in)

- **ChangeField** — updates a field on the model
- **SendNotification** — sends a Laravel notification to a user
- **DispatchEvent** — dispatches a Laravel event

### Conditions

[](#conditions)

**Operators**: `equals`, `not_equals`, `greater_than`, `less_than`, `greater_than_or_equal`, `less_than_or_equal`, `contains`, `not_contains`, `is_null`, `is_not_null`, `in`, `not_in`

**Logic**: `and` (all conditions must match) or `or` (any condition can match)

Extending
---------

[](#extending)

### Custom Triggers

[](#custom-triggers)

Implement `TriggerInterface`. The `configSchema()` method returns Filament form fields that appear in the node settings dialog when this trigger type is selected:

```
use Codenzia\FilamentWorkflow\Engine\Contracts\TriggerInterface;
use Filament\Forms\Components\Select;
use Illuminate\Database\Eloquent\Model;

class TaskStatusChangedTrigger implements TriggerInterface
{
    public static function label(): string
    {
        return 'Task Status Changed';
    }

    public function matches(Model $model, array $config, array $context): bool
    {
        $changedFields = $context['changed_fields'] ?? [];

        if (! in_array('status', $changedFields)) {
            return false;
        }

        if (isset($config['to'])) {
            $newValue = $context['new']['status'] ?? null;
            if ($newValue != $config['to']) {
                return false;
            }
        }

        return true;
    }

    public static function configSchema(): array
    {
        return [
            Select::make('config.from')
                ->label('From Status')
                ->placeholder('Any')
                ->options(TaskStatusEnum::class),
            Select::make('config.to')
                ->label('To Status')
                ->placeholder('Any')
                ->options(TaskStatusEnum::class),
        ];
    }
}
```

### Custom Actions

[](#custom-actions)

Implement `ActionHandlerInterface`. The `configSchema()` method defines what the user configures on the action node:

```
use Codenzia\FilamentWorkflow\Engine\Contracts\ActionHandlerInterface;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Toggle;
use Illuminate\Database\Eloquent\Model;

class AssignUserAction implements ActionHandlerInterface
{
    public static function label(): string
    {
        return 'Assign User';
    }

    public function execute(Model $model, array $config, array $context): array
    {
        $userId = $config['user_id'] ?? null;
        if (! $userId) {
            return ['error' => 'No user specified'];
        }

        $model->update(['assigned_to_user_id' => $userId]);

        return ['action' => 'assign_user', 'to_user_id' => $userId];
    }

    public static function configSchema(): array
    {
        return [
            Select::make('config.user_id')
                ->label('Assign to User')
                ->options(fn () => User::pluck('name', 'id')->all())
                ->searchable(),
            Select::make('config.role')
                ->label('Or Assign by Project Role')
                ->options([
                    'owner' => 'Project Owner',
                    'lead' => 'Team Lead',
                ]),
        ];
    }
}
```

**Config field naming**: Use the `config.` prefix (e.g., `config.user_id`, `config.status`) so values are stored in the node's `config` JSON column. The node type forms automatically show/hide config fields based on the selected trigger/action type.

### Register in ServiceProvider

[](#register-in-serviceprovider)

```
WorkflowEngine::registerTrigger('task.status_changed', TaskStatusChangedTrigger::class);
WorkflowEngine::registerAction('assign_user', AssignUserAction::class);
```

Workflow Templates
------------------

[](#workflow-templates)

Provide predefined workflow templates by subclassing `WorkflowDesigner` and overriding `getWorkflowTemplates()`:

```
use Codenzia\FilamentWorkflow\Pages\WorkflowDesigner;

class TaskWorkflowDesigner extends WorkflowDesigner
{
    protected function getWorkflowTemplates(): array
    {
        return [
            'auto_close_parent' => [
                'name' => 'Auto-Close Parent Task',
                'description' => 'When all subtasks are completed, close the parent.',
                'icon' => 'heroicon-o-check-circle',
                'nodes' => [
                    [
                        'node_type' => 'trigger',
                        'type_config' => 'subtasks.all_closed',
                        'label' => 'All Subtasks Closed',
                        'config' => [],
                        'position_x' => 300,
                        'position_y' => 100,
                    ],
                    [
                        'node_type' => 'action',
                        'type_config' => 'close_parent',
                        'label' => 'Close Parent',
                        'config' => [],
                        'position_x' => 300,
                        'position_y' => 350,
                    ],
                ],
                'connections' => [
                    ['source' => 0, 'target' => 1],
                ],
            ],
        ];
    }
}
```

When templates are available, the "New Workflow" dialog shows a **Start from template** dropdown. Selecting a template pre-fills the name and description, and scaffolds all nodes and connections on creation.

Templates reference node indices (0, 1, 2...) for connections — the `source` and `target` values map to the `nodes` array order.

Customizing the Designer
------------------------

[](#customizing-the-designer)

The `WorkflowDesigner` is designed for subclassing. Override these methods to customize:

MethodPurpose`getWorkflowTemplates()`Provide predefined workflow templates`getCreateWorkflowSchema()`Form fields for "New Workflow" dialog`getEditWorkflowSchema()`Form fields for "Settings" dialog`mapCreateData(array $data)`Transform create form data → DB attributes`mapEditData(array $data)`Transform edit form data → DB attributes`fillEditForm(Workflow $workflow)`Populate edit form from model`getRulesTree()`Customize the rules view tree structure`getStepSummary(WorkflowNode $node)`Customize step card descriptions`onRulesStepDoubleClick(int $nodeId)`Customize double-click behavior on rules steps`addStepAction()`Customize the "Add Step" modal form`computeAutoLayout()`Customize auto-positioning algorithmExample — embedding a subclass:

```
@livewire(
    \App\Filament\Pages\TaskWorkflowDesigner::class,
    ['projectId' => $project->id, 'modelType' => Task::class]
)
```

Remember to register the Livewire component in your `AppServiceProvider`:

```
Livewire::component('app.filament.pages.task-workflow-designer', TaskWorkflowDesigner::class);
```

Time-Based Triggers
-------------------

[](#time-based-triggers)

For triggers based on due dates or overdue status, schedule the command:

```
// routes/console.php
Schedule::command('workflow:process-time-triggers')->everyFifteenMinutes();
```

The command finds active workflows with time-based trigger nodes, queries matching models, and dispatches evaluation jobs. Deduplication prevents the same model+workflow from re-triggering within the configured window (default: 24 hours).

### Time Trigger Query Scoping

[](#time-trigger-query-scoping)

For time-based triggers to work with the preview and scheduler, implement the optional `scopeMatchingModels` static method on your trigger class:

```
class TaskOverdueTrigger implements TriggerInterface
{
    // ... matches() and configSchema() methods ...

    /**
     * Scope query to find models that match this time trigger.
     * Used by the scheduler command and the designer's preview panel.
     */
    public static function scopeMatchingModels(Builder $query, array $config): Builder
    {
        $daysOverdue = (int) ($config['days_overdue'] ?? 0);

        return $query
            ->whereNotNull('due_date')
            ->where('due_date', '
