PHPackages                             qnox/workflows - 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. qnox/workflows

ActiveLibrary

qnox/workflows
==============

Configurable, GUI-driven workflow engine for Laravel (dynamic action keywords)

v1.0.0(4d ago)03↑2900%MITPHPPHP &gt;=8.3

Since Aug 19Pushed todayCompare

[ Source](https://github.com/laurenttandika/qnox_workflows)[ Packagist](https://packagist.org/packages/qnox/workflows)[ RSS](/packages/qnox-workflows/feed)WikiDiscussions main Synced today

READMEChangelog (1)Dependencies (5)Versions (3)Used By (0)

qnox/workflows
==============

[](#qnoxworkflows)

Laravel workflow package for configurable approval flows with ordered levels, per-level track history, assignee resolution, and setup-driven actions.

Install
-------

[](#install)

```
composer require qnox/workflows
php artisan vendor:publish --tag=qnox-workflows-config
php artisan vendor:publish --tag=qnox-workflows-migrations
php artisan vendor:publish --tag=qnox-workflows-views # only when overriding UI
php artisan migrate
```

After installation, the default settings dashboard is:

```
/settings/workflows

```

The prefix, middleware, route names, API routes, and every package view are configurable in `config/workflows.php`.

For production, add your authorization middleware to the settings route group:

```
'routes' => [
    'web' => [
        'middleware' => ['web', 'auth', 'can:workflows.manage'],
    ],
],
```

Define the `workflows.manage` ability with a Laravel gate or create the equivalent permission when using `spatie/laravel-permission`.

Concepts
--------

[](#concepts)

- `Workflow`: the whole process definition
- `WorkflowLevel`: a step inside the process
- `WorkflowAssignment`: who can act on a level
- `WorkflowTransition`: an allowed action from one level to another
- `WorkflowInstance`: one running record for a subject
- `WorkflowInstanceLevel`: track/history rows for the running instance
- `WorkflowAction`: the actions actually taken

Subject Model
-------------

[](#subject-model)

The workflow instance belongs to a polymorphic `subject`. The subject is the business resource being reviewed or approved.

Examples of a subject:

- `LeaveRequest`
- `PurchaseRequisition`
- `Invoice`
- `Tender`

The user is not the subject unless the thing being approved is actually a user record.

In this package:

- `subject` = the resource under workflow
- `initiator` = the user who started the workflow
- `actor` = the user who performs an action on a level

The package stores the subject through:

- `workflow_instances.subject_type`
- `workflow_instances.subject_id`

Example:

```
$instance = app(WorkflowEngine::class)->start(
    $purchaseRequisition,   // subject
    $workflow,
    auth()->user(),         // initiator
    ['amount' => $purchaseRequisition->amount]
);
```

Basic Usage
-----------

[](#basic-usage)

```
use Qnox\Workflows\Services\WorkflowEngine;
use Qnox\Workflows\Models\Workflow;

$engine = app(WorkflowEngine::class);
$workflow = Workflow::where('slug', 'application-approval')->firstOrFail();

$instance = $engine->start($application, $workflow, auth()->user(), [
    'amount' => 5000000,
    'country' => 'TZ',
]);

$actions = $engine->availableActions($instance, auth()->user());
$engine->act($instance, 'submit', auth()->user(), ['comment' => 'Submitted to supervisor']);
```

Setup a Workflow
----------------

[](#setup-a-workflow)

Create a workflow, then define levels, assignments, and transitions.

```
use Qnox\Workflows\Models\Workflow;
use Qnox\Workflows\Models\WorkflowLevel;
use Qnox\Workflows\Models\WorkflowAssignment;
use Qnox\Workflows\Models\WorkflowTransition;

$workflow = Workflow::create([
    'workflow_group_id' => 1,
    'name' => 'Application Approval',
    'slug' => 'application-approval',
    'is_active' => true,
]);

$applicantLevel = WorkflowLevel::create([
    'workflow_id' => $workflow->id,
    'name' => 'Applicant',
    'sequence' => 1,
    'is_start' => true,
    'description' => 'Application owner prepares and submits',
]);

$supervisorLevel = WorkflowLevel::create([
    'workflow_id' => $workflow->id,
    'name' => 'Supervisor Review',
    'sequence' => 2,
    'description' => 'Supervisor reviews the application',
    'is_approval' => true,
]);

$financeLevel = WorkflowLevel::create([
    'workflow_id' => $workflow->id,
    'name' => 'Finance Approval',
    'sequence' => 3,
    'description' => 'Finance approves and closes',
    'is_terminal' => true,
    'can_close' => true,
]);
```

Setup Assignments
-----------------

[](#setup-assignments)

Assignments decide who is allowed to act on a level.

Applicant-owned first level:

```
WorkflowAssignment::create([
    'workflow_level_id' => $applicantLevel->id,
    'criteria' => ['initiator' => true],
]);
```

Direct user assignment:

```
WorkflowAssignment::create([
    'workflow_level_id' => $supervisorLevel->id,
    'assignable_type' => App\Models\User::class,
    'assignable_id' => 12,
]);
```

Criteria-based assignment:

```
WorkflowAssignment::create([
    'workflow_level_id' => $financeLevel->id,
    'criteria' => [
        'department_id' => 3,
        'permissions' => ['payments.approve'],
    ],
]);
```

Setup Transitions
-----------------

[](#setup-transitions)

Actions are configured in `workflow_transitions`. The engine does not generate fallback actions.

- Each transition defines `action_key`, `label`, `direction`, `to_level_id`, and optional `status`.
- `to_level_id` may be `null` for same-level or terminal actions.
- Use `status` to control the instance/history status after the action.
- Use `meta.complete = true` to explicitly mark an action as terminal.
- Use `meta.mark_submitted = true` to stamp `submitted_at` when needed.
- The first level can still be applicant-owned by using assignment criteria `['initiator' => true]`.

Example transitions:

```
WorkflowTransition::create([
    'workflow_id' => $workflow->id,
    'from_level_id' => $applicantLevel->id,
    'to_level_id' => $supervisorLevel->id,
    'action_key' => 'submit',
    'label' => 'Submit',
    'direction' => 'forward',
    'status' => 'in_progress',
    'meta' => ['mark_submitted' => true],
]);

WorkflowTransition::create([
    'workflow_id' => $workflow->id,
    'from_level_id' => $supervisorLevel->id,
    'to_level_id' => $financeLevel->id,
    'action_key' => 'approve',
    'label' => 'Approve',
    'direction' => 'forward',
    'status' => 'approved',
]);

WorkflowTransition::create([
    'workflow_id' => $workflow->id,
    'from_level_id' => $supervisorLevel->id,
    'to_level_id' => $applicantLevel->id,
    'action_key' => 'return',
    'label' => 'Return for Update',
    'direction' => 'backward',
    'status' => 'returned',
]);

WorkflowTransition::create([
    'workflow_id' => $workflow->id,
    'from_level_id' => $financeLevel->id,
    'to_level_id' => null,
    'action_key' => 'complete',
    'label' => 'Complete',
    'direction' => 'stay',
    'status' => 'completed',
    'meta' => ['complete' => true],
]);
```

Return or reject to a specific previous level:

```
WorkflowTransition::create([
    'workflow_id' => $workflow->id,
    'from_level_id' => $financeLevel->id,
    'to_level_id' => $applicantLevel->id,
    'action_key' => 'reject_to_applicant',
    'label' => 'Reject to Applicant',
    'direction' => 'backward',
    'status' => 'rejected',
]);

WorkflowTransition::create([
    'workflow_id' => $workflow->id,
    'from_level_id' => $financeLevel->id,
    'to_level_id' => $supervisorLevel->id,
    'action_key' => 'return_to_supervisor',
    'label' => 'Return to Supervisor',
    'direction' => 'backward',
    'status' => 'returned',
]);
```

This allows a level to send the workflow back to any earlier level, not only the immediately previous one.

Retrieve Configured Flows
-------------------------

[](#retrieve-configured-flows)

Get all workflow definitions:

```
use Qnox\Workflows\Models\Workflow;

$workflows = Workflow::query()
    ->with(['levels.assignments', 'transitions'])
    ->where('is_active', true)
    ->orderBy('name')
    ->get();
```

Get one workflow with its full setup:

```
$workflow = Workflow::query()
    ->with([
        'levels.assignments',
        'levels.outgoingTransitions',
        'transitions',
    ])
    ->where('slug', 'application-approval')
    ->firstOrFail();
```

Get the ordered level flow:

```
$levels = $workflow->levels()
    ->with(['assignments', 'outgoingTransitions.toLevel'])
    ->orderBy('sequence')
    ->get();
```

Retrieve the Current Flow
-------------------------

[](#retrieve-the-current-flow)

Get the active workflow instance for a subject:

```
use Qnox\Workflows\Models\WorkflowInstance;

$instance = WorkflowInstance::query()
    ->with(['workflow', 'currentLevel', 'history.level', 'actions'])
    ->where('subject_type', $application::class)
    ->where('subject_id', $application->getKey())
    ->latest('id')
    ->first();
```

Load the underlying subject resource from the instance:

```
$subject = $instance?->subject;
```

Get the current level of that instance:

```
$currentLevel = $instance?->currentLevel;
$currentStatus = $instance?->status;
```

Get the current track/history row:

```
$currentTrack = $instance?->history()
    ->whereNull('exited_at')
    ->latest('id')
    ->first();
```

Get the actions available to the current user:

```
$actions = app(WorkflowEngine::class)->availableActions($instance, auth()->user());
```

Act on the Current Flow
-----------------------

[](#act-on-the-current-flow)

Run a configured action:

```
$updated = app(WorkflowEngine::class)->act(
    $instance,
    'approve',
    auth()->user(),
    ['comment' => 'Reviewed and approved']
);
```

API Routes
----------

[](#api-routes)

The package registers:

- `GET /api/workflows/instances/{instance}/actions`
- `POST /api/workflows/instances/{instance}/act`
- `GET /api/workflow-instances/{instance}/actions`
- `POST /api/workflow-instances/{instance}/act`

The last two endpoints are v0 compatibility aliases and can be disabled with `workflows.routes.api.legacy_routes`.

Example POST payload:

```
{
  "action_key": "approve",
  "payload": {
    "comment": "Looks good"
  }
}
```

Status Notes
------------

[](#status-notes)

Statuses are stored on `workflow_instances.status`, `workflow_instance_levels.status`, and `workflow_actions.status`.

Common values are:

- `pending`
- `in_progress`
- `approved`
- `returned`
- `rejected`
- `on_hold`
- `recalled`
- `completed`

You may also use your own status values in transitions if your application needs different labels.

Version 1 Administration
------------------------

[](#version-1-administration)

Version 1 provides web configuration screens for:

- Module groups
- Business modules
- Workflow definitions
- Ordered workflow levels
- Configured actions and transitions
- Number formats and sequences
- Workflow instance history and action modals
- Per-user workflow inboxes and sidebar counters

Add the settings link to a Blade menu:

```
@can('workflows.manage')
    Workflow Settings
@endcan
```

For applications with a menu registry:

```
use Qnox\Workflows\Support\WorkflowMenu;

$menu->register(WorkflowMenu::items());
```

The package does not mutate the host application's navigation. This keeps it compatible with Blade sidebars, AdminLTE, Spatie menus, Livewire navigation, and custom menu tables.

Workflow Inbox and Sidebar Counters
-----------------------------------

[](#workflow-inbox-and-sidebar-counters)

The package materializes one `WorkflowInboxItem` for every resolved user when a workflow enters a level. This makes counters fast and records whether each recipient opened or responded to the assignment.

Default inbox:

```
/workflows/inbox

```

Named links:

```
workflows.inbox.index
workflows.inbox.new
workflows.inbox.pending
workflows.inbox.attended
workflows.inbox.responded
workflows.inbox.held
workflows.inbox.ended
workflows.inbox.counts

```

Categories mean:

- `new`: assigned but not yet opened
- `pending`: currently assigned and not yet answered
- `attended`: opened but not yet answered
- `responded`: the user performed a workflow action
- `held`: a workflow related to the user is currently on hold
- `ended`: a workflow related to the user has completed

Get all counters:

```
use Qnox\Workflows\Services\WorkflowInbox;

$counts = app(WorkflowInbox::class)->counts(auth()->user());
```

Example:

```
[
    'new' => 4,
    'pending' => 7,
    'attended' => 2,
    'responded' => 16,
    'held' => 1,
    'ended' => 42,
]
```

Add links directly to a Blade sidebar:

```
@php($workflowCounts = app(\Qnox\Workflows\Services\WorkflowInbox::class)->counts(auth()->user()))

    New workflows
    @if($workflowCounts['new'])
        {{ $workflowCounts['new'] }}
    @endif

    Responded
    {{ $workflowCounts['responded'] }}

```

Or use the package menu descriptors:

```
$items = WorkflowMenu::inbox(auth()->user());
```

For asynchronous sidebar updates:

```
GET /workflows/inbox/counts
```

The response contains the six counter values as JSON. The default inbox is provided by `workflows::inbox.index` and can be replaced:

```
'views' => [
    'inbox' => 'my-application.workflows.inbox',
],
```

Approvers also receive `NextApproverNotification`. Configure its channels:

```
'notify_channels' => ['mail', 'database'],
```

Mail notifications link to `workflows.inbox.show`; database notifications contain the workflow instance, workflow, subject, current level, and status identifiers. Notifications are queued after the workflow transaction commits.

Inbox routes have their own middleware configuration:

```
'routes' => [
    'inbox' => [
        'enabled' => true,
        'prefix' => 'workflows/inbox',
        'middleware' => ['web', 'auth'],
    ],
],
```

Modules and Groups
------------------

[](#modules-and-groups)

`WorkflowGroup` organizes related business functions. `WorkflowModule` represents a business process, and `Workflow` represents one executable definition.

```
$group = WorkflowGroup::create([
    'name' => 'Finance',
    'slug' => 'finance',
]);

$module = WorkflowModule::create([
    'workflow_group_id' => $group->id,
    'name' => 'Payment Requisitions',
    'slug' => 'payment-requisitions',
]);

$workflow = Workflow::create([
    'workflow_group_id' => $group->id,
    'workflow_module_id' => $module->id,
    'name' => 'Standard Payment Approval',
    'slug' => 'standard-payment-approval',
    'is_active' => true,
]);
```

A module may be attached to application configuration through `moduleable_type` and `moduleable_id`, for example a `PaymentRequisitionType`.

Model Integration
-----------------

[](#model-integration)

Add `HasWorkflows` to resources that participate in workflows:

```
use Qnox\Workflows\Concerns\HasWorkflows;

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

Then start and retrieve workflows through the resource:

```
$instance = $requisition->startWorkflow(
    $workflow,
    auth()->user(),
    [
        'amount' => $requisition->amount,
        'department_id' => $requisition->department_id,
    ],
);

$current = $requisition->currentWorkflowInstance();
```

Lifecycle Events
----------------

[](#lifecycle-events)

Version 1 dispatches:

- `WorkflowStarting`
- `WorkflowStarted`
- `WorkflowActioning`
- `WorkflowActioned`
- `WorkflowLevelEntered`
- `WorkflowLevelExited`
- `WorkflowClaimed`
- `WorkflowCompleted`
- `WorkflowRejected`
- `WorkflowReturned`
- `WorkflowHeld`
- `WorkflowResumed`
- `WorkflowRecalled`

The `Starting` and `Actioning` events run synchronously and may stop the operation by throwing an exception. All other events are registered after the workflow database transaction commits.

Application-specific behavior belongs in listeners:

```
use Qnox\Workflows\Events\WorkflowCompleted;

class MarkPaymentApproved
{
    public function handle(WorkflowCompleted $event): void
    {
        $payment = $event->instance->subject;

        if ($payment instanceof PaymentRequisition) {
            $payment->update([
                'status' => 'approved',
                'approved_at' => now(),
            ]);
        }
    }
}
```

Register it in the host application's event provider:

```
protected $listen = [
    WorkflowCompleted::class => [
        MarkPaymentApproved::class,
    ],
];
```

This replaces the large module-ID `switch` subscriber used by IOO-WEB-V2.

Polymorphic Assignments
-----------------------

[](#polymorphic-assignments)

Assignments may target any model:

```
WorkflowAssignment::create([
    'workflow_level_id' => $level->id,
    'type' => 'position',
    'assignable_type' => Position::class,
    'assignable_id' => $position->id,
]);
```

The host application teaches the package how to resolve that model:

```
use Qnox\Workflows\Contracts\AssignmentProvider;

class PositionAssignmentProvider implements AssignmentProvider
{
    public function users(WorkflowAssignment $assignment, array $context = []): Collection
    {
        return User::where('position_id', $assignment->assignable_id)->get();
    }

    public function contains(
        Authenticatable $user,
        WorkflowAssignment $assignment,
        array $context = []
    ): bool {
        return (string) $user->position_id === (string) $assignment->assignable_id;
    }
}
```

Register providers in the published configuration:

```
'assignment_providers' => [
    'user' => UserAssignmentProvider::class,
    'position' => App\Workflows\PositionAssignmentProvider::class,
    'designation' => App\Workflows\DesignationAssignmentProvider::class,
    'unit' => App\Workflows\UnitAssignmentProvider::class,
    'department' => App\Workflows\DepartmentAssignmentProvider::class,
],
```

Expose the types in the settings form:

```
'assignment_options' => [
    'user' => ['label' => 'User', 'model' => App\Models\User::class],
    'position' => ['label' => 'Position', 'model' => App\Models\Position::class],
    'designation' => ['label' => 'Designation', 'model' => App\Models\Designation::class],
    'unit' => ['label' => 'Unit', 'model' => App\Models\Department::class],
],
```

Use a morph map to avoid storing application class names:

```
Relation::enforceMorphMap([
    'user' => User::class,
    'position' => Position::class,
    'unit' => Department::class,
    'payment-requisition' => PaymentRequisition::class,
]);
```

Level Participation and Assignment Modes
----------------------------------------

[](#level-participation-and-assignment-modes)

Participation permissions and routing assignments are separate:

- `WorkflowLevelParticipant` answers who is permitted to view, attend, or act at a level.
- `WorkflowAssignment` answers where a particular workflow item is routed.
- `WorkflowInstanceLevel::assigned_to` records who owns the current track.

When both participant permissions and routing assignments exist, recipients are the intersection:

```
Users resolved from the routed unit/department
∩
Users permitted for the workflow level
=
Users who receive the workflow item

```

If a level has no participant permissions, its existing assignments remain the eligibility source for backward compatibility.

Each level has one assignment mode:

```
pooled
automatic
direct

```

### Pooled

[](#pooled)

The track enters the level without an owner. All eligible users receive a New inbox item. The first authorized user to select **Attend this workflow** atomically claims it. Other participants' inbox entries close, and only the claiming user may act.

```
$level->update(['assignment_mode' => 'pooled']);
```

Claim route:

```
POST workflows.inbox.claim

```

The claim operation locks the active `WorkflowInstanceLevel`, so two users cannot claim the same item.

### Automatic

[](#automatic)

The first eligible resolved user is assigned immediately. Only that user receives the active inbox item and may act.

```
$level->update(['assignment_mode' => 'automatic']);
```

### Direct

[](#direct)

The current action must provide `payload.next_user_id`. The selected user must satisfy both routing and level-participation rules.

```
'form_schema' => [
    'fields' => [
        [
            'name' => 'next_user_id',
            'type' => 'number',
            'label' => 'Next user ID',
            'required' => true,
        ],
    ],
],
```

The engine rejects a direct transition when the selected user is not eligible.

User Workflow Permissions
-------------------------

[](#user-workflow-permissions)

The package includes the equivalent of IOO-WEB-V2's User → Workflow Permissions screen:

```
/settings/workflows/participants
/settings/workflows/participants/users/{user}

```

Named routes:

```
workflows.participants.index
workflows.participants.user
workflows.participants.user.update

```

Link to it from an existing user-management screen:

```

    Workflow Permissions

```

The administrator can grant a user access to individual workflow levels. Definition screens also provide participant configuration with separate View, Attend, and Act permissions.

Non-user participant types such as positions and units use providers:

```
'participant_options' => [
    'user' => ['label' => 'User', 'model' => App\Models\User::class],
    'position' => ['label' => 'Position', 'model' => App\Models\Position::class],
    'unit' => ['label' => 'Unit', 'model' => App\Models\Department::class],
],

'participant_providers' => [
    'user' => UserParticipantProvider::class,
    'position' => App\Workflows\PositionParticipantProvider::class,
    'unit' => App\Workflows\UnitParticipantProvider::class,
],
```

Configurable Number Generation
------------------------------

[](#configurable-number-generation)

Number sequences replace the IOO sysdef/table-name switch with editable formats and atomic counters.

```
NumberSequence::create([
    'key' => 'payment-requisition',
    'name' => 'Payment Requisition Number',
    'format' => '{prefix}/{year}/{number}',
    'prefix' => 'QNOX/PR',
    'next_value' => 1,
    'padding' => 6,
    'reset_period' => 'yearly',
    'is_active' => true,
]);
```

Generate a number:

```
$number = app(NumberGenerator::class)->next('payment-requisition');
// QNOX/PR/2026/000001
```

Or use the model convenience trait:

```
use Qnox\Workflows\Concerns\HasWorkflowNumber;

class PaymentRequisition extends Model
{
    use HasWorkflowNumber;
}

$number = $payment->generateWorkflowNumber('payment-requisition');
```

Supported format tokens are:

```
{prefix} {number} {year} {year:2} {month} {day}
{module} {department} {unit} {tenant} {subject_id}

```

Custom tokens receive their values from context:

```
$number = app(NumberGenerator::class)->next('department-document', [
    'department' => 'FIN',
    'module' => 'PR',
]);
```

Valid reset periods are `never`, `yearly`, `monthly`, and `daily`. Generation uses a database transaction and `lockForUpdate()`, preventing duplicate numbers under concurrent requests.

Scoped counters are supported:

```
$number = app(NumberGenerator::class)->next(
    'payment-requisition',
    ['department' => $department->code],
    $department,
);
```

Create one `NumberSequence` record for each scope. The scope is stored polymorphically.

Custom Views and Action Modals
------------------------------

[](#custom-views-and-action-modals)

Views load under the `workflows::` namespace. Override a screen without publishing:

```
'views' => [
    'instance' => 'payments.workflows.show',
    'action_modal' => 'payments.workflows.action-modal',
],
```

Or publish all views:

```
php artisan vendor:publish --tag=qnox-workflows-views
```

Published files are written to:

```
resources/views/vendor/workflows

```

Action modals are generated from `WorkflowTransition::form_schema`:

```
'form_schema' => [
    'confirmation' => 'Approve and forward this request?',
    'fields' => [
        [
            'name' => 'comment',
            'type' => 'textarea',
            'label' => 'Comments',
            'required' => true,
        ],
    ],
],
```

To render package actions inside a custom resource view:

```
@include('workflows::actions.buttons', [
    'instance' => $instance,
    'actions' => app(WorkflowEngine::class)
        ->availableActions($instance, auth()->user()),
])
```

Manual Testing Checklist
------------------------

[](#manual-testing-checklist)

1. Open `/settings/workflows` and create a group, module, definition, levels, and transitions.
2. Create assignments for the start and approval levels.
3. Start a workflow using `startWorkflow()` or `WorkflowEngine::start()`.
4. Sign in as the resolved approver and open `/workflows/inbox/new`.
5. For a pooled level, confirm all eligible participants see New, then click Attend.
6. Confirm the item disappears from the other participants and only the claimant can act.
7. For an automatic level, confirm only the resolved owner receives the item.
8. For a direct level, select an eligible `next_user_id` and confirm invalid users are rejected.
9. Opening an owned item moves it from `new` to `attended`.
10. Perform an action from the generated modal.
11. Confirm the item appears under `responded` and the next approver receives a `new` item.
12. Complete the workflow and confirm it appears under `ended`.
13. Create a number format under Workflow Settings → Number Formats.
14. Generate several numbers and confirm padding and reset behavior.

###  Health Score

43

—

FairBetter than 89% of packages

Maintenance100

Actively maintained with recent releases

Popularity4

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity53

Maturing project, gaining track record

 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

4d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/114920778?v=4)[Laurent Timothy Tandika](/maintainers/laurenttandika)[@laurenttandika](https://github.com/laurenttandika)

---

Top Contributors

[![laurenttandika](https://avatars.githubusercontent.com/u/114920778?v=4)](https://github.com/laurenttandika "laurenttandika (12 commits)")

### Embed Badge

![Health badge](/badges/qnox-workflows/health.svg)

```
[![Health](https://phpackages.com/badges/qnox-workflows/health.svg)](https://phpackages.com/packages/qnox-workflows)
```

###  Alternatives

[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)[fleetbase/core-api

Core Framework and Resources for Fleetbase API

1239.7k25](/packages/fleetbase-core-api)[laravel/pulse

Laravel Pulse is a real-time application performance monitoring tool and dashboard for your Laravel application.

1.7k16.3M160](/packages/laravel-pulse)[api-platform/laravel

API Platform support for Laravel

58190.1k21](/packages/api-platform-laravel)[aedart/athenaeum

Athenaeum is a mono repository; a collection of various PHP packages

265.2k](/packages/aedart-athenaeum)

PHPackages © 2026

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