Search by

packstub / filament-flow

Visual workflow automations for Filament panels: triggers, conditions and actions drawn on a canvas, run by your queue.

Maintainers

Package info

github.com/packstub/filament-flow

Documentation

pkg:composer/packstub/filament-flow

Transparency log

Fund package maintenance!

icaliman

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

v1.1.0 2026-09-03 08:38 UTC

README

Filament Flow — visual workflow automation for Filament panels

Latest Version on Packagist Tests Total Downloads License

Visual workflow automations for your Filament panel: draw triggers, conditions and actions on a canvas, and let the runner — or your queue — do the rest.

Features

  • Visual builder — a drag-and-drop canvas inside a Filament resource: add nodes from a sidebar, connect them, branch on true / false, and edit each node's settings in a slide-over built from Filament form components.
  • Triggers — record created / updated / deleted (only when chosen attributes change, from one value to another, once per record, or not more than once every N days), spatie model-state transitions, user registered, any Laravel event, a cron schedule with its own timezone and catch-up, a signed webhook, a manual run, a Run workflow action on any resource, or a call from another workflow.
  • Conditions — compare a record attribute (including "changed from / to") or any two values with twenty operators, combine several rules with AND / OR, or check the time of day.
  • Actions — send an email, a Filament database notification, a Slack, Discord, Teams or Telegram message, an SMS or WhatsApp message through Twilio, call an HTTP endpoint and use its response in later nodes, create or update records, assign an owner, add tags, transition a state, find records and loop over them, wait for a duration or until a date, ask for approval or wait for a signal from your code, call another workflow, or write to the log. Every action has retries, a continue-on-error switch and an optional error branch; a workflow can name an on-failure workflow.
  • Placeholders{{ model.name }}, {{ webhook.order.id }}, {{ last.body.id }}, {{ model.url }} and friends, with filters such as | date:Y-m-d, | upper and | default:none, resolved from the run's payload wherever you type text.
  • Secrets — an encrypted store for API tokens and webhook URLs, used as {{ secrets.slack_webhook }} in actions only and masked in run logs; per tenant when you need it.
  • Approvals & signals — an Ask for approval step pauses the run until a person decides from a notification, an email link or the Approvals page, with Approved / Rejected / Timed out outputs; Wait for signal pauses it until your code calls Flow::signal(). Waits are database rows, so they survive deploys and queue restarts.
  • Multi-tenant — in a panel with tenancy each team manages its own workflows, secrets, runs and approvals, global workflows run for every tenant, and a maxWorkflows() hook plugs plan limits in.
  • Versioned — every change to a workflow is kept with who saved it and a summary of what changed; compare and restore from the Versions tab; runs pin the version they ran.
  • Run history — every run is stored with its status, trigger, record, payload summary, a step-by-step log with timings and outputs, and any error, browsable from a Runs tab and a cross-workflow Runs page with stats; Test (dry run), Run now and Run again buttons; jump from a failed step to its node on the canvas; per-workflow retention and a "deactivate after N consecutive failures" guard.
  • Queue & scheduling — run workflows inline or on your queue (dispatched after your transaction commits), pause them for minutes or days with a Wait step, and start them from cron expressions with one scheduler entry.
  • Webhooks — a tokenised, optionally HMAC-signed POST endpoint per workflow that answers 202 Accepted and exposes the request body to your nodes.
  • Safe by default — outgoing requests cannot reach private networks unless you allow it, record updates respect mass-assignment rules, credential headers never reach the run log, hidden model attributes never reach a template, and the Workflows resource can sit behind a policy, a Gate ability or a callback.
  • Extensible — write your own trigger, action or condition class with a Filament form schema and register it on the plugin, in the config, or with Flow::register().
  • Dark mode ready and translatable — the canvas follows Filament's theme, and every string, node name and description lives in a language file.

Compatibility

Plugin Filament Laravel PHP
1.x 4.x, 5.x 12.x, 13.x 8.3+

Installation

composer require packstub/filament-flow
php artisan packstub-flow:install

Add the trait to the models you want to automate, and the plugin to your panel:

use Packstub\Flow\Concerns\HasWorkflows;

class Order extends Model
{
    use HasWorkflows;
}
use Packstub\Flow\FlowPlugin;

$panel->plugin(
    FlowPlugin::make()
        ->navigationGroup('Automation'),
);

A Workflows resource appears in the panel navigation.

The Workflows table with triggers, run counts and the last run's status

Full walkthrough: Installation.

The builder

A workflow is a graph: one or more trigger nodes, followed by actions and conditions connected by edges. Draw it on the canvas — add nodes with the + button, the right-click menu or the plus on an unconnected output handle, drag handles together to connect them, and use a condition's True and False outputs to branch.

A finished workflow on the canvas

Every node has a label, a description and its own settings, edited in a slide-over you open with the gear icon or a double-click. The graph is saved with the workflow as plain JSON (nodes and edges), so it is easy to seed, export and test.

use Packstub\Flow\Filament\Forms\Components\FlowBuilder;

// The canvas is a form field — use it in your own resource if you like
FlowBuilder::make('definition')->minHeight('70vh')

Read more: Building workflows.

Triggers

A trigger starts a run and decides what the payload contains. Pick one from the sidebar and configure it in its settings — the model for a record trigger, a cron expression for a schedule, the event class for an event trigger.

The add-node sidebar listing the available triggers

Trigger Fires when
Manual You press Run now, use a Run workflow action on a resource, run packstub-flow:run, or call Flow::run()
Schedule A cron expression is due (checked every minute by packstub-flow:cron, with optional catch-up of missed minutes)
Webhook A POST hits /flow/webhooks/{workflow}/{token}
Record created / updated / deleted A model using HasWorkflows is created, updated or deleted — optionally only when given attributes change from one value to another, once per record, or not more than once every N days
Date on a record A date column, shifted by an offset, is reached: "3 days before due_at", "1 hour after starts_at"
State transitioned / Status changed A spatie/laravel-model-states state or spatie/laravel-model-status status changes (offered when the package is installed)
User registered Laravel's Illuminate\Auth\Events\Registered event is dispatched
Event Any Laravel event of the configured class is dispatched
Called by another workflow Another workflow runs a Call workflow action pointing at this one

A workflow can have several triggers; the run starts from the one that fired. Fire your own from code:

use Packstub\Flow\Facades\Flow;
use Packstub\Flow\Nodes\Triggers\RecordUpdated;

Flow::dispatch(RecordUpdated::class, ['model' => $order]);

Any Filament resource can start a workflow for a record with RunWorkflowAction (and RunWorkflowBulkAction for a selection):

->recordActions([\Packstub\Flow\Filament\Actions\RunWorkflowAction::make()])

Read more: Triggers.

Conditions

A condition has a True and a False output; the run follows the branch that matches. Record attribute checks a column, accessor or dotted relationship path (customer.country) on the record that started the run, and knows whether it changed, changed from or changed to a value; Compare values compares any two values, placeholders allowed on both sides; Multiple conditions joins several rules with AND / OR; Time of day is true inside a daily window in a timezone of your choice.

Operators: equals, does not equal, greater / less than (or equal), contains, does not contain, starts with, ends with, is one of, is empty, is not empty, is true, is false, is null, is not null, matches regular expression, is a date before / after.

Read more: Conditions.

Actions

Actions do the work. Each one's settings are a small Filament form; text fields accept placeholders.

The settings slide-over for a Send email action

Action What it does
Send email A plain Markdown email to one or more addresses, with an optional button ({{ model.url }} opens the record in the panel)
Send notification A Filament database notification to panel users, picked by email, with an optional action button
Send Slack / Discord / Teams / Telegram message Posts to a Slack or Discord webhook, a Teams Workflows webhook, or a Telegram bot chat
Send SMS (Twilio) An SMS or WhatsApp message through the Twilio Messages API
HTTP request Calls any URL with headers and a JSON body; can fail the run on a 4xx / 5xx response
Update record Sets attributes on the record that started the run, quietly by default
Create record Creates a record, on its own or through a relationship of the current one
Assign owner Sets a user on the record: a fixed one, or round robin over a list
Add tag Attaches, detaches or syncs spatie/laravel-tags tags
Transition state Moves a spatie/laravel-model-states state to another one
Find records / For each Queries records and runs a branch once per item ({{ item.* }})
Ask for approval Pauses until an approver decides — Approved / Rejected / Timed out outputs, an Approvals page, notification and email links
Wait for signal Pauses until your code calls Flow::signal('payment.42', [...])
Wait Pauses the run for seconds, minutes, hours or days; the rest continues through the queue
Call workflow Runs another workflow with the current payload
Write to log Writes a line to the application log at the chosen level

Find records and For each give a workflow a loop: query records, run a branch once per item with {{ item.* }}, then continue from Done.

A Find records node feeding a For each loop with its Each item and Done outputs

Read more: Actions.

Placeholders

Any text a node sends or compares can reference the run's payload with {{ path }}. Paths are resolved with data_get(), so relationships and nested arrays just work:

Order {{ model.reference }} for {{ model.customer.name }} is now {{ model.status }}
Previously {{ original.status }} — changed: {{ changes.status }}
Webhook said {{ webhook.order.total }}; the event carrier is {{ event.carrier }}

{{ record.x }} is an alias of {{ model.x }}; {{ model.url }} is the record's page in the panel. Each node's settings panel lists the placeholders its trigger provides.

Read more: Placeholders.

Secrets

Tokens and webhook URLs live on the Secrets page, encrypted with your app key and never shown again. Actions reference them as {{ secrets.slack_webhook }}; conditions and triggers cannot read them, and every resolved value is masked in the run log. In a panel with tenancy each tenant keeps its own secrets.

The Secrets page listing keys, never values

Read more: Secrets.

Approvals & signals

Two actions pause a run until something outside the workflow happens. Ask for approval waits for a person: approvers are notified in the panel and by email, and the run continues along Approved, Rejected or Timed out. Wait for signal waits for your code.

An Ask for approval node with Approved, Rejected and Timed out outputs

Pending requests are listed on the Approvals page, with a badge counting the ones waiting for the signed-in user.

The Approvals page with a pending request

use Packstub\Flow\Facades\Flow;

// Continue every run waiting on this key along its "Received" output
Flow::signal('payment.42', ['paid_at' => now()]);

Read more: Approvals & signals.

Runs

Every run is recorded: status (Running, Waiting, Succeeded, Failed), the trigger that started it, a summary of the payload, when it started and how long it took, a step-by-step log, and the error message if something threw. The Runs tab under a workflow lists them and refreshes on its own.

The Runs tab under a workflow

Open a run to see each step in order.

A run's steps and payload in the details modal

Test on the edit page performs a dry run — conditions evaluated, side effects simulated and logged as "would run". Run now (in the table and on the edit page) starts an active workflow from its first trigger, and packstub-flow:run does the same from the console. The Runs page lists every run across workflows with stats and a jump to the failing node on the canvas. packstub-flow:prune deletes finished runs older than the configured retention (or the workflow's own). A workflow can switch itself off after N consecutive failures, notify your admins, and hand failures to an on-failure workflow.

The Runs page with its stats and filters

php artisan packstub-flow:run "Welcome sequence" --payload='{"answer": 42}'
php artisan packstub-flow:prune --days=30

Read more: Runs.

Queue & scheduling

By default a workflow runs inside the request or model event that triggered it. Set PACKSTUB_FLOW_QUEUE=true to push every run onto the queue instead — optionally on its own connection and queue name. Wait steps always continue through the queue, so run a worker whenever you use them.

PACKSTUB_FLOW_QUEUE=true
PACKSTUB_FLOW_QUEUE_NAME=flows

Schedule triggers are evaluated by packstub-flow:cron, which the plugin registers with Laravel's scheduler every minute. A running schedule:work (or a cron entry for schedule:run) is all you need.

Read more: Queue & scheduling.

Webhooks

A Webhook trigger gives the workflow a secret token. Send a POST to /flow/webhooks/{workflow id}/{token} and the JSON body is available to every node as {{ webhook.* }}. The endpoint answers 202 Accepted with the run id and status, and 404 for an unknown workflow, an inactive one or a wrong token.

curl -X POST https://example.com/flow/webhooks/9d2f.../your-secret-token \
  -H 'Content-Type: application/json' \
  -d '{"order": {"id": 42, "status": "shipped"}}'

The prefix, middleware (api and throttle:60,1 by default) and an on/off switch live in the config.

Read more: Webhooks.

Multi-tenancy

In a panel with ->tenant(Team::class) the Workflows and Secrets resources, the Runs and Approvals pages are scoped to the current tenant, and new workflows belong to it. When a trigger fires, the tenant of the record decides which workflows run: the tenant's own plus the global ones. Tell the runtime where a payload's tenant comes from with a resolver or a relationship name, and cap workflows per tenant with maxWorkflows().

FlowPlugin::make()
    ->resolveTenantUsing(fn (array $payload) => $payload['model']?->team)
    ->maxWorkflows(fn (?Team $team): ?int => $team?->plan->limit('workflows'))

Read more: Multi-tenancy.

Extending

A node is a class with a name, an icon, a Filament form schema and one method: matches() for a trigger, evaluate() for a condition, handle() for an action.

use Filament\Forms\Components\TextInput;
use Packstub\Flow\Nodes\Action;
use Packstub\Flow\Nodes\Concerns\InterpolatesPlaceholders;

class AssignToTeam extends Action
{
    use InterpolatesPlaceholders;

    public function getName(): string
    {
        return 'Assign to team';
    }

    public function getIcon(): ?string
    {
        return 'heroicon-o-user-group';
    }

    public function getFormSchema(): array
    {
        return [TextInput::make('team')->required()];
    }

    public function handle(array $config, array $payload): void
    {
        $payload['model']->update(['team' => $this->interpolate($config['team'], $payload)]);
    }
}
FlowPlugin::make()->actions([AssignToTeam::class])

Read more: Extending.

Configuration

use Packstub\Flow\FlowPlugin;

FlowPlugin::make()
    ->triggers([OrderPaid::class])                 // add your own nodes
    ->actions([AssignToTeam::class])
    ->conditions([IsVip::class])
    ->without([SendSlackMessage::class])           // hide built-in nodes
    ->models([Order::class, Invoice::class])       // offered by the record triggers
    ->navigationGroup('Automation')
    ->navigationIcon('heroicon-o-bolt')
    ->navigationSort(10)
    ->resource(App\Filament\Resources\WorkflowResource::class)   // or ->withoutResource()

Read more: Configuration — the config file, tables and models, queue, webhooks, execution limits and retention.

Documentation

The same pages live in the docs/ directory of this repository.

Testing

composer test

Changelog

See the changelog.

Security vulnerabilities

Please e-mail support@packstub.dev rather than opening a public issue.

Credits

License

MIT. See the license file.