PHPackages                             imtiaz-hasan/flowforge - 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. [Queues &amp; Workers](/categories/queues)
4. /
5. imtiaz-hasan/flowforge

ActiveLibrary[Queues &amp; Workers](/categories/queues)

imtiaz-hasan/flowforge
======================

Flowforge - an n8n-style workflow automation engine for Laravel. Define triggers, actions, and branching flows in pure PHP.

v0.1.0(1mo ago)10MITPHPPHP ^8.2|^8.3

Since Jul 8Pushed 1mo agoCompare

[ Source](https://github.com/Imtiaz-Hasan/flowforge)[ Packagist](https://packagist.org/packages/imtiaz-hasan/flowforge)[ Docs](https://github.com/Imtiaz-Hasan/flowforge)[ RSS](/packages/imtiaz-hasan-flowforge/feed)WikiDiscussions master Synced 1w ago

READMEChangelog (1)Dependencies (10)Versions (2)Used By (0)

[![Latest Version](https://camo.githubusercontent.com/6e2ef3d0193374c1c1fd3ad289c094fb8e57de11582c92c79ff06d92c3e66362/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f696d7469617a2d686173616e2f666c6f77666f726765)](https://packagist.org/packages/imtiaz-hasan/flowforge)[![Total Downloads](https://camo.githubusercontent.com/922326da74ba1714f5e3faa0bd1d3f6176f94fdb1268a93da45ebbfe14f565ed/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f696d7469617a2d686173616e2f666c6f77666f726765)](https://packagist.org/packages/imtiaz-hasan/flowforge)[![License](https://camo.githubusercontent.com/99081b16d7defaf8a07d23a53122e942b0f555d7b40042e7399975ba49fd56d6/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f696d7469617a2d686173616e2f666c6f77666f726765)](LICENSE)

Flowforge
=========

[](#flowforge)

An n8n-style workflow automation engine for Laravel. Define triggers, actions, and branching flows in pure PHP.

Flowforge lets you wire up workflows out of small, connected nodes. A trigger starts a run, data flows from one node to the next, conditions branch the path, and every step is logged. It runs on your queue, retries failed steps, and is built to be extended with your own node types.

```
 trigger ──> [ transform ] ──> < condition > ──true──> [ http_request ] ──> [ send_email ]
                                     │
                                     └──false──> [ run_job ]

```

Why
---

[](#why)

n8n is great, but it lives outside your app. Flowforge keeps the automation inside your Laravel codebase: your models, your queue, your jobs, your tests. No separate service to host, no JSON exported from a visual editor that nobody can code-review. You define flows in PHP, commit them, and run them like any other part of the app.

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

[](#requirements)

- PHP 8.2 or 8.3
- Laravel 11 or 12

Install
-------

[](#install)

```
composer require imtiaz-hasan/flowforge
```

Publish and run the migrations:

```
php artisan vendor:publish --tag=flowforge-migrations
php artisan migrate
```

Optionally publish the config:

```
php artisan vendor:publish --tag=flowforge-config
```

Your first workflow in 5 lines
------------------------------

[](#your-first-workflow-in-5-lines)

```
use Flowforge\Facades\Flowforge;

Flowforge::define('welcome')
    ->webhook()
    ->email('greet', ['to' => '{{ trigger.email }}', 'subject' => 'Welcome', 'body' => 'Glad you are here.'])
    ->save();
```

That registers a webhook-triggered workflow. POST to `/flowforge/webhooks/welcome` with `{"email": "ada@example.com"}` and Flowforge starts a run on your queue, sends the email, and records the result.

Run a stored workflow by hand:

```
Flowforge::run('welcome', ['email' => 'ada@example.com']);
```

How it works
------------

[](#how-it-works)

A workflow is a set of nodes keyed by id. Each node has a type, a config, and a link to the next node. The engine starts at the first node and walks the graph:

1. Resolve the node's config, filling in `{{ placeholders }}` from earlier output.
2. Run the node, retrying up to its configured number of attempts.
3. Write a node-run record (status, input, output, error, timing).
4. Follow the node's link, or a condition's branch, to the next node.

The whole run is one queued `RunWorkflowJob` that walks the graph. A delay node saves a cursor and re-dispatches the job on a delay, so a flow can wait without holding a worker.

### Passing data between nodes

[](#passing-data-between-nodes)

Any node's output is available to later nodes by id. Use `{{ trigger.x }}` for the payload that started the run, and `{{ nodeId.field }}` for an earlier node's output.

```
Flowforge::define('lookup')
    ->http('user', ['method' => 'GET', 'url' => 'https://api.example.com/users/{{ trigger.id }}'])
    ->email('notify', [
        'to' => '{{ user.body.email }}',
        'subject' => 'Your profile',
        'body' => 'Hello {{ user.body.name }}',
    ])
    ->save();
```

A lone placeholder such as `{{ trigger.id }}` keeps its original type (int, array, bool). A placeholder inside a longer string is interpolated as text.

### Branching

[](#branching)

`condition()` takes a comparison and two closures, one for each branch. The first node added inside a closure is that branch's entry point.

```
Flowforge::define('route-by-plan')
    ->condition(
        'is_pro',
        ['left' => '{{ trigger.plan }}', 'operator' => '==', 'right' => 'pro'],
        fn ($flow) => $flow->email('pro', ['to' => '{{ trigger.email }}', 'subject' => 'Pro', 'body' => 'Thanks for going pro.']),
        fn ($flow) => $flow->email('free', ['to' => '{{ trigger.email }}', 'subject' => 'Welcome', 'body' => 'Enjoy the free plan.']),
    )
    ->save();
```

Operators: `==`, `===`, `!=`, `>`, `>=`, `schedule('0 * * * *')`the cron expression is dueModel event`->onModel(User::class, ['created'])`a model fires that Eloquent eventManual`->manual()`you call `Flowforge::run()` or the artisan commandFor model triggers, add the `TriggersFlowforge` trait to the model:

```
use Flowforge\Triggers\TriggersFlowforge;

class User extends Model
{
    use TriggersFlowforge;
}
```

Webhooks can require a shared secret. Pass it to `->webhook('my-secret')` and send it as the `X-Flowforge-Secret` header (or a `secret` query parameter). A mismatch returns 403.

Built-in nodes
--------------

[](#built-in-nodes)

TypeBuilderWhat it does`http_request``->http($id, $config)`Sends an HTTP request with configurable method, headers, query, and JSON body. Output: status, ok, body, headers.`send_email``->email($id, $config)`Sends a plain-text email (to, subject, body).`run_job``->runJob($id, $config)`Dispatches a queued job class with a payload array.`transform``->transform($id, $set)`Builds a new output array, resolving placeholders. Useful for shaping data.`condition``->condition($id, $config, $true, $false)`Compares two values and branches.`delay``->delay($id, $seconds)`Pauses the run, then resumes from where it left off.`loop``->loop($id, $items, $type, $config)`Runs one inner node once per item in a list.The `http_request` node also accepts an `auth` block:

```
->http('call', [
    'url' => 'https://api.example.com/me',
    'auth' => ['type' => 'bearer', 'token' => '{{ trigger.token }}'],
    // or ['type' => 'basic', 'username' => '...', 'password' => '...']
])
```

The `send_email` node sends plain text by default, or HTML when you set `html`, and accepts `cc`, `bcc`, `from`, and `reply_to`.

### Looping over a list

[](#looping-over-a-list)

```
Flowforge::define('import')
    ->loop('rows', '{{ trigger.users }}', 'http_request', [
        'method' => 'POST',
        'url' => 'https://api.example.com/users',
        'json' => ['name' => '{{ item.name }}', 'position' => '{{ index }}'],
    ])
    ->save();
```

Inside the loop config, `{{ item }}` is the current element and `{{ index }}` is its position. The loop runs in order, in the same process, and collects each run's output under `results`. For very large lists, fan out with queued jobs instead.

### Per-node error handling

[](#per-node-error-handling)

Each node config accepts:

- `retries`: how many extra attempts on failure (defaults to `flowforge.node_retries`).
- `continue_on_error`: if true, a failed node is logged and the workflow keeps going instead of stopping.

Custom nodes
------------

[](#custom-nodes)

This is where Flowforge earns its keep. A node is any class that implements the `Node` contract. Scaffold one with `php artisan make:flowforge-node SlackNode`, or write it by hand:

```
use Flowforge\Contracts\Node;
use Flowforge\Engine\NodeContext;
use Flowforge\Engine\NodeResult;

class SlackNode implements Node
{
    public function handle(NodeContext $context, array $config): NodeResult
    {
        Http::post($config['webhook_url'], ['text' => $config['text']]);

        return NodeResult::ok(['posted' => true]);
    }
}
```

Register it (in a service provider's `boot`):

```
Flowforge::registerNode('slack', SlackNode::class);
```

Now use it in any workflow:

```
Flowforge::define('alert')
    ->node('ping', 'slack', ['webhook_url' => config('services.slack.hook'), 'text' => 'Deploy finished'])
    ->save();
```

The engine resolves placeholders in `$config` before calling `handle()`, so your node receives plain values. Return `NodeResult::ok($output)` to continue, `NodeResult::branch('true', $output)` to branch, or `NodeResult::pause($seconds, $output)` to wait.

Storing workflows as data
-------------------------

[](#storing-workflows-as-data)

The fluent builder is one way in. You can also store a workflow as an array or JSON and load it from the database. The structure is the same either way: a `start` node id and a map of nodes, each with `type`, `config`, and `next` (or `next_true` / `next_false` for conditions).

Events
------

[](#events)

The engine fires events you can listen for, so logging, metrics, and alerting stay out of the engine:

- `Flowforge\Events\WorkflowStarted` (run begins, not on a resume)
- `Flowforge\Events\NodeCompleted` (each node finishes, success or failure)
- `Flowforge\Events\WorkflowCompleted` (run finishes successfully)
- `Flowforge\Events\WorkflowFailed` (a node failed and did not continue on error)

```
Event::listen(WorkflowFailed::class, function ($event) {
    Log::error("Workflow {$event->run->workflow_key} failed at node {$event->nodeId}: {$event->error}");
});
```

Read-only UI
------------

[](#read-only-ui)

Flowforge ships an optional read-only UI that lists workflows and shows each run node by node. It is off by default. Turn it on in `config/flowforge.php` (or set `FLOWFORGE_UI_ENABLED=true`) and visit `/flowforge`.

Run logs can hold payloads and email addresses, so the UI is guarded. With no rule set, only the `local` environment may view it. Define who can see it in production from a service provider's `boot`:

```
use Flowforge\Flowforge;

Flowforge::auth(fn ($request) => $request->user()?->isAdmin());
```

Reliability
-----------

[](#reliability)

- **Validation.** Definitions are checked on save: a missing start node, a link to a node that does not exist, an unknown node type, or a cycle is rejected with a clear error instead of failing mid-run.
- **Per-node retries.** Set `retries` on a node, or a default with `flowforge.node_retries`.
- **Failed runs.** A run that fails records the error and the node it failed at. Resume it from that node with `flowforge:retry`.
- **Stuck runs.** If a worker dies hard, the run is swept up by `flowforge:reap` after `flowforge.run_timeout_minutes`. Schedule it to run regularly.

Artisan commands
----------------

[](#artisan-commands)

```
php artisan flowforge:list                       # list stored workflows
php artisan flowforge:run {key} --payload='{}'   # run one by key
php artisan flowforge:demo                        # create a side-effect-free demo workflow
php artisan flowforge:retry {run}                 # retry a failed run from the node that failed
php artisan flowforge:reap --minutes=15           # fail runs stuck running past the timeout
php artisan flowforge:prune --days=30             # delete old runs (add --keep-failed to keep failures)
php artisan make:flowforge-node SlackNode         # scaffold a custom node class
```

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

[](#configuration)

`config/flowforge.php` covers the queue connection and name, the default node retry count, the run timeout used by `flowforge:reap`, HTTP client timeouts and retries, the webhook route prefix and middleware, and the UI toggle, prefix, and middleware. Every secret comes from the environment. Nothing is hardcoded.

Testing
-------

[](#testing)

The package is tested with [Orchestra Testbench](https://github.com/orchestral/testbench) on an in-memory SQLite database. External calls are faked.

```
composer install
vendor/bin/phpunit
```

The suite needs the `pdo_sqlite` extension. CI runs it on PHP 8.2 and 8.3 against Laravel 11 and 12.

Roadmap
-------

[](#roadmap)

- A visual flow editor for the UI (read-only today).
- More built-in nodes (database write, queue fan-out).
- A UI for model-event triggers.
- Sub-workflows (a node that runs another workflow).
- Parallel branches that run concurrently.

License
-------

[](#license)

MIT. See [LICENSE](LICENSE).

###  Health Score

35

—

LowBetter than 77% of packages

Maintenance90

Actively maintained with recent releases

Popularity2

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity38

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

48d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/743b36cde1d6466ef7f1af52fa49687355235e57e2f742d99e27e3ee3542791a?d=identicon)[Imtiaz-Hasan](/maintainers/Imtiaz-Hasan)

---

Top Contributors

[![Imtiaz-Hasan](https://avatars.githubusercontent.com/u/78067334?v=4)](https://github.com/Imtiaz-Hasan "Imtiaz-Hasan (5 commits)")

---

Tags

automationlaravelpackagistphpqueueworkflow-enginelaravelautomationqueueworkflowenginepipelineno-code

###  Code Quality

TestsPHPUnit

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/imtiaz-hasan-flowforge/health.svg)

```
[![Health](https://phpackages.com/badges/imtiaz-hasan-flowforge/health.svg)](https://phpackages.com/packages/imtiaz-hasan-flowforge)
```

###  Alternatives

[laravel/scout

Laravel Scout provides a driver based solution to searching your Eloquent models.

1.7k57.2M688](/packages/laravel-scout)[psalm/plugin-laravel

Psalm plugin for Laravel

3345.4M354](/packages/psalm-plugin-laravel)[laravel/cashier

Laravel Cashier provides an expressive, fluent interface to Stripe's subscription billing services.

2.5k31.8M163](/packages/laravel-cashier)[laravel/ai

The official AI SDK for Laravel.

1.1k4.6M341](/packages/laravel-ai)[api-platform/laravel

API Platform support for Laravel

58190.1k21](/packages/api-platform-laravel)[anousss007/vigilance

A driver-agnostic control center for Laravel queues, jobs, commands and the scheduler. Monitor what ran (with parameters), see failures, and dispatch jobs or run artisan commands manually from a self-contained dashboard.

1949.9k](/packages/anousss007-vigilance)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
