---
title: "Installation — Workflows for Laravel | Roundly"
description: "Install via Composer, publish config and the eight migrations, settle key types, schedule workflows:wake, register a subject type and start a workflow."
url: https://roundly-consulting.com/open-source/docs/workflows-for-laravel/installation
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Workflows for Laravel](https://roundly-consulting.com/open-source/docs/workflows-for-laravel.md)

# Installation

Require the package, publish its config and migrations with the install command, and migrate. The service provider and the Workflows facade alias are auto-discovered, together with the providers of the Roundly packages it builds on — crypto (idempotency digests), enums and package-toolkit:

```bash
composer require roundly-consulting/workflows-for-laravel roundly-consulting/approvals-for-laravel
php artisan workflows:install                              # publishes config/workflows.php and the eight migrations
php artisan vendor:publish --tag="approvals-migrations"    # approvals powers the approval step in the example below
php artisan migrate
```

workflows:install refuses to run when any of the package’s tables already exists. It prints the scheduler lines and the subject-registration hint. Migrations are publish-only (never auto-loaded) and forward-only.

approvals-for-laravel powers the approval step in the example below; leave it and its migrations out if you don’t need approvals. Without it, publishing a graph with an approval step fails adapter\_missing.

## 1\. Settle the key types before migrating

The polymorphic columns are sized at migration time. With bigint ids everywhere the defaults fit; with UUID or ULID keys, set them before php artisan migrate:

```bash
# Before php artisan migrate — bigint, uuid or ulid
WORKFLOWS_KEY_TYPE=bigint           # user morphs: started_by, published_by, requested_by
WORKFLOWS_OWNER_KEY_TYPE=           # owner morphs; blank = WORKFLOWS_KEY_TYPE
WORKFLOWS_SUBJECT_KEY_TYPE=         # subject morphs; blank = WORKFLOWS_KEY_TYPE
WORKFLOWS_PRIMARY_KEY_TYPE=bigint   # the package's own ids
```

A model whose key does not fit the configured type is refused before any SQL with InvalidWorkflowsConfigurationException naming the key — PostgreSQL never answers with a raw “invalid input syntax”. workflows:doctor checks this too.

## 2\. Schedule wake and prune

Delays, timeouts, retries, escalations and schedules wake on the scheduler. Nothing is scheduled automatically — without this line a delay never ends:

```php
// routes/console.php
use Illuminate\Support\Facades\Schedule;

Schedule::command('workflows:wake')->everyMinute()->withoutOverlapping(10)->onOneServer();
Schedule::command('workflows:prune')->daily();
```

workflows:wake fires due timers, retries, wait timeouts, condition polls, escalations and schedule triggers, reclaims expired leases and re-pushes lost jobs. onOneServer() needs a cache store shared by every server (redis, database, memcached, dynamodb). Correctness never depends on it — every claim is a compare-and-set.

## 3\. Run a queue worker

With workflows.queue.enabled on (the default) each step runs as a job — RunStepJob, ResumeStepJob, FireEscalationJob; ids only, tries = 1, dispatched after commit. The job timeout is engine.lease − 30 s (570 s by default): the queue connection’s retry\_after must be longer than that, and the worker needs pcntl to enforce it. Laravel’s default database connection has retry\_after 90 s, so a fresh app raises it — or lowers engine.lease if every step is short. workflows:doctor fails on a too-short retry\_after and warns without pcntl. With the queue off, steps run inline in the request that starts or resumes them, through a trampoline (no recursion), bounded by limits.max\_steps.

```bash
# A step job may run for up to 570 s (engine.lease − 30 s): retry_after must be longer.
DB_QUEUE_RETRY_AFTER=600        # Laravel's default database queue (90 s otherwise)
REDIS_QUEUE_RETRY_AFTER=600     # the redis queue
php artisan workflows:doctor    # fails on a too-short retry_after
```

## 4\. Register a subject type and use the trait

A subject is the model an instance is about. Register each type once, in a service provider’s boot():

```php
// AppServiceProvider::boot()
use RoundlyConsulting\Workflows\Facades\Workflows;

Workflows::subjects()->register(
    Workflows::subject('purchase_request', PurchaseRequest::class)
        ->label('Purchase request')
        ->ownedBy('team')
        ->field('amount', 'decimal', 'Amount')
        ->field('requester', 'party', 'Requester'),
);
```

```php
use RoundlyConsulting\Workflows\Traits\HasWorkflows;

class PurchaseRequest extends Model
{
    use HasWorkflows;   // $request->workflows(), touch on update, cancel on delete
}
```

## 5\. Define, publish and start a workflow

```php
use RoundlyConsulting\Workflows\Facades\Workflows;
use RoundlyConsulting\Workflows\Graph\Build\{Party, Trigger, When};

// Once, in a service provider: what a purchase request looks like to the engine.
Workflows::subjects()->register(
    Workflows::subject('purchase_request', PurchaseRequest::class)
        ->label('Purchase request')
        ->ownedBy('team')
        ->field('amount', 'decimal', 'Amount')
        ->field('requester', 'party', 'Requester')
        ->field('approver', 'party', 'Approver'),
);

// The workflow itself is data: a JSON graph your users edit in a builder — or written in PHP.
$definition = Workflows::definitions()->create(key: 'purchase-approval', name: 'Purchase approval',
    subjectType: 'purchase_request', owner: $team);

Workflows::definition('purchase-approval', $team)->edit(Workflows::graph()   // or ->edit(json_decode($json, true))
    ->trigger('start', Trigger::manual())
    ->condition('check', When::field('subject.amount')->gt('500'))
    ->approval('approve', approvers: [Party::field('subject.approver')])
    ->end('done')
    ->connect('start', 'check')
    ->connect('check', 'approve', on: 'true')
    ->connect('check', 'done', on: 'false')
    ->connect('approve', 'done', on: ['approved', 'rejected', 'expired']));
Workflows::definition('purchase-approval', $team)->publish(by: $admin);

// Start it (this graph's trigger is `manual`) — with an `event` trigger the app event starts it instead.
$result = Workflows::start('purchase-approval', subject: $request, owner: $team)
    ->by($request->requester)
    ->idempotencyKey("submit:{$request->id}")
    ->dispatch();

$result->instance->status;   // InstanceStatus::Running — a queue worker runs it on to the approval step, where it waits
```

## Publish tags

| Tag | Publishes |
| --- | --- |
| `workflows-config` | config/workflows.php |
| `workflows-migrations` | The eight migrations, timestamped on publish. |
| `workflows-translations` | lang/vendor/workflows/{en,sk} |
| `workflows-routes` | The opt-in inbound webhook route file. |

```bash
php artisan vendor:publish --tag="workflows-config"         # config/workflows.php
php artisan vendor:publish --tag="workflows-migrations"     # the eight migrations, timestamped on publish
php artisan vendor:publish --tag="workflows-translations"   # lang/vendor/workflows/{en,sk}
php artisan vendor:publish --tag="workflows-routes"         # the opt-in inbound webhook route file
```

## Tables

| Table | Holds |
| --- | --- |
| `workflow_definitions` | Named workflows per owner: key, name, subject type key, status, current version, webhook id. |
| `workflow_versions` | Numbered graph snapshots: graph, trigger, actor, settings, checksum, review flags, publisher. |
| `workflow_instances` | Runs: version, owner, subject, trigger, status, current node, failure, parent and depth. |
| `workflow_step_runs` | The step log: one row per attempt of a visit, with input, output, outcome, error and lease. |
| `workflow_waits` | Timers, events, condition polls, retries and timeouts the engine wakes on. |
| `workflow_escalations` | The next pending escalation level of each waiting visit. |
| `workflow_idempotency_keys` | Start and effect keys, stored as digests. |
| `workflow_dry_runs` | Logged dry runs (only with ->log() or dry\_run.log on). |

Table names are configurable (workflows.tables.\*); every column is listed under Database.

## Translations

English and Slovak ship under the workflows:: namespace — enums, exceptions, failures, fields, steps and validation. Step labels, validation messages, failure messages and enum labels all translate.

## Check the setup

```bash
php artisan workflows:doctor            # --json for CI and health checks, --strict to fail on warnings
php artisan about --only=workflows        # step types, bridges, queue mode, key types, limits, webhooks
```

php artisan about shows a Workflows section: the number of step types, installed bridges, the queue mode, the four key types, the limits and whether the webhook route is on.

## Upgrading from 1.0.0

No migration and no config change. Check three things:

- The queue connection’s retry\_after must be above the 570 s step job timeout (DB\_QUEUE\_RETRY\_AFTER=600 for the database queue) — run workflows:doctor, which fails otherwise.
- Graphs with an approval step need approvals-for-laravel: require it and publish its migrations (approvals-migrations) before migrating.
- Workflows::emit() no longer throws when one matching definition refuses to start: the others still start and the refusal is listed in EmitResult::$failed. Code that caught that exception should read $result->failed instead.

## Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

[More ways to support, including crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/workflows-for-laravel.md)
