---
title: "Configuration — Workflows for Laravel | Roundly"
description: "Every key of config/workflows.php with its env variable, default and range — key types, queue, limits, retries, authorization, webhooks, pruning."
url: https://roundly-consulting.com/open-source/docs/workflows-for-laravel/configuration
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)

# Configuration

workflows:install publishes config/workflows.php; to publish it alone:

```bash
php artisan vendor:publish --tag="workflows-config"
```

Every key is read through strict readers: a wrong type throws InvalidWorkflowsConfigurationException naming the key, a value outside its accepted range throws naming the key and the range, and a blank value (an env KEY=) takes the default. Every configuration error, the toolkit readers’ included, is that one class, so a single catch covers them. Durations are ISO 8601 strings. Class settings must name an existing class implementing the contract.

## The config file

```php
<?php

declare(strict_types=1);

use RoundlyConsulting\Workflows\Http\Resources\WorkflowOwnerResource;
use RoundlyConsulting\Workflows\Http\Resources\WorkflowSubjectResource;
use RoundlyConsulting\Workflows\Http\Resources\WorkflowUserResource;
use RoundlyConsulting\Workflows\Models\StepRun;
use RoundlyConsulting\Workflows\Models\WorkflowDefinition;
use RoundlyConsulting\Workflows\Models\WorkflowDryRun;
use RoundlyConsulting\Workflows\Models\WorkflowEscalation;
use RoundlyConsulting\Workflows\Models\WorkflowIdempotencyKey;
use RoundlyConsulting\Workflows\Models\WorkflowInstance;
use RoundlyConsulting\Workflows\Models\WorkflowVersion;
use RoundlyConsulting\Workflows\Models\WorkflowWait;
use RoundlyConsulting\Workflows\Support\GateStepAuthorizer;

/*
| Every key is read through the package's strict readers: a typo throws naming the key, and a
| blank value (a host's `KEY=`) takes the default. Pass env values through raw.
*/

return [

    /*
    | Key types of the polymorphic columns: `bigint`, `uuid` or `ulid`. `key_type` governs the
    | user morphs (started_by, published_by, requested_by); a blank `owner_key_type` or
    | `subject_key_type` falls back to it. Every registered subject type must use the subject
    | key type. `primary_key_type` is the type of the package's own ids.
    */

    'key_type' => env('WORKFLOWS_KEY_TYPE', 'bigint'),

    'owner_key_type' => env('WORKFLOWS_OWNER_KEY_TYPE'),

    'subject_key_type' => env('WORKFLOWS_SUBJECT_KEY_TYPE'),

    'primary_key_type' => env('WORKFLOWS_PRIMARY_KEY_TYPE', 'bigint'),

    /*
    | Swappable models: extend the packaged class and name the subclass here.
    */

    'models' => [
        'definition' => WorkflowDefinition::class,
        'version' => WorkflowVersion::class,
        'instance' => WorkflowInstance::class,
        'step_run' => StepRun::class,
        'wait' => WorkflowWait::class,
        'escalation' => WorkflowEscalation::class,
        'idempotency_key' => WorkflowIdempotencyKey::class,
        'dry_run' => WorkflowDryRun::class,
    ],

    'tables' => [
        'definitions' => 'workflow_definitions',
        'versions' => 'workflow_versions',
        'instances' => 'workflow_instances',
        'step_runs' => 'workflow_step_runs',
        'waits' => 'workflow_waits',
        'escalations' => 'workflow_escalations',
        'idempotency_keys' => 'workflow_idempotency_keys',
        'dry_runs' => 'workflow_dry_runs',
    ],

    /*
    | Every step runs as a queued job. With the queue off, the same steps run inline, one after
    | another (an iterative runner, never nested dispatches). Blank connection / queue = the
    | application defaults.
    */

    'queue' => [
        'enabled' => env('WORKFLOWS_QUEUE_ENABLED', true),
        'connection' => env('WORKFLOWS_QUEUE_CONNECTION'),
        'queue' => env('WORKFLOWS_QUEUE_NAME'),
    ],

    // Host step types: a list of classes extending RoundlyConsulting\Workflows\Steps\StepType.
    'steps' => [],

    // Subject types: a list of classes implementing Contracts\DescribesWorkflowSubject.
    'subjects' => [],

    // Laravel events mapped to trigger keys: [Event::class => 'request.submitted'], or
    // [Event::class => ['key' => 'request.submitted', 'subject' => 'request', 'user' => 'user',
    //     'payload' => ['amount', 'currency'], 'idempotency_key' => 'submissionId']], or a
    // self-describing WorkflowEvent as [Event::class => '*']. Workflows::triggers()->map() and
    // ->listen() do the same from a service provider.
    'events' => [],

    // The default timezone of a workflow version (blank = app.timezone).
    'timezone' => env('WORKFLOWS_TIMEZONE'),

    'engine' => [
        // How long a claimed step may run before wake reclaims it (the job timeout is 30 s less).
        'lease' => 'PT10M',
        // A queued step whose job was pushed longer ago than this is pushed again by wake.
        'requeue_after' => 'PT5M',
    ],

    'wake' => [
        'batch' => 500,
        'claim_ttl' => 'PT10M',
    ],

    'retry' => [
        'default' => [
            'max_attempts' => 3,
            'backoff' => [30, 120, 600],
            'jitter' => 0.1,
        ],
        'max_attempts_cap' => 10,
    ],

    'limits' => [
        'max_steps' => 200,
        'max_depth' => 3,
        // Instances a definition may start per minute; blank = 60, `off` = no limit.
        'max_starts_per_minute' => env('WORKFLOWS_MAX_STARTS_PER_MINUTE', 60),
        'max_loop_iterations' => 1000,
    ],

    'graph' => [
        'max_nodes' => 500,
        'max_edges' => 2000,
        'max_bytes' => 524288,
    ],

    'conditions' => [
        'max_depth' => 6,
        'max_clauses' => 100,
    ],

    'steps_output' => [
        'max_bytes' => 65536,
    ],

    // Each step run records its input: the node config with every value reference resolved,
    // as the handler saw it. A larger input is stored as {"truncated": true, "bytes": n}.
    'steps_input' => [
        'max_bytes' => 65536,
    ],

    'escalations' => [
        'max_levels' => 5,
    ],

    'idempotency' => [
        // Trigger keys expire after this; a key re-used later starts a new instance.
        'ttl' => 'P30D',
    ],

    'schedule' => [
        // Missed schedule slots within this window are coalesced into one start.
        'catch_up' => 'PT1H',
    ],

    'authorization' => [
        // class-string<Contracts\AuthorizesSteps>: called before every side-effect step and escalation
        // action, a null ability included (the default allows that).
        'authorizer' => GateStepAuthorizer::class,
        // Step type key => policy ability, overriding the type's own ability().
        'abilities' => [],
    ],

    'actor' => [
        // class-string of an invokable returning the service-account user the `system` actor
        // authorizes as (null: a null user, so policies deny unless they accept ?User).
        'system_user' => null,
    ],

    'owners' => [
        // class-string<Contracts\RestoresOwnerContext>; null = teams when installed, else none.
        'context' => null,
        // class-string<Contracts\ChecksOwnerMembership>; null = teams when installed, else allow.
        'membership' => null,
    ],

    'parties' => [
        // The user model `{"kind":"user"}` parties resolve to (null = auth.providers.users.model).
        'user_model' => null,
        // class-string<Contracts\ResolvesRoles>; null = teams when installed, else no roles.
        'roles' => null,
    ],

    'adapters' => [
        // class-string<Contracts\TransitionAdapter>; null = the lifecycle bridge when installed.
        'transition' => null,
        // class-string<Contracts\DeliversWebhooks>; null = no outbound webhook step.
        'webhooks' => null,
    ],

    'business_time' => [
        // ISO weekday numbers of the built-in `weekdays` calendar (1 = Monday).
        'weekdays' => [1, 2, 3, 4, 5],
        'max_search_days' => 366,
        // class-string of an invokable returning the owner model of app-wide definitions'
        // opening-hours calendars.
        'calendar_owner' => null,
    ],

    'approval' => [
        // Safety net after an approval round's expiry, in case its resolution event was lost.
        'expiry_grace' => 'PT5M',
    ],

    // What happens to running instances when their subject is deleted: cancel | keep.
    'subjects_on_deleted' => 'cancel',

    'bridges' => [
        'lifecycle' => [
            // Emit `lifecycle.<lifecycle>.<transition>` for every lifecycle state change.
            'emit_transitions' => env('WORKFLOWS_LIFECYCLE_EMIT_TRANSITIONS', 'off'),
        ],
    ],

    // Definition files (`workflows:sync` / `workflows:export`): absolute or relative to the base path.
    'sync' => [
        'path' => env('WORKFLOWS_SYNC_PATH', 'resources/workflows'),
    ],

    'dry_run' => [
        // Write a workflow_dry_runs row for every dry run (on | off).
        'log' => env('WORKFLOWS_DRY_RUN_LOG', 'off'),
    ],

    'webhooks' => [
        // The opt-in inbound start route: POST {prefix}/hooks/{hook}.
        'enabled' => env('WORKFLOWS_WEBHOOKS_ENABLED', 'off'),
        'prefix' => 'workflows',
        'middleware' => ['api'],
        // REQUIRED when enabled: the signature check (e.g. 'sentinel.signed:<profile>'). An
        // empty list keeps the route unregistered (fail closed).
        'verify_middleware' => [],
        'max_bytes' => 262144,
    ],

    'resources' => [
        // How a host model renders. Point a key at your own JsonResource to add a name or avatar.
        'owner' => WorkflowOwnerResource::class,     // receives the owner model
        'subject' => WorkflowSubjectResource::class, // receives the subject model
        'user' => WorkflowUserResource::class,       // receives a user model
    ],

    'prune' => [
        'instances_after' => 'P90D',
        'dry_runs_after' => 'P30D',
    ],

];
```

## Key types, models and tables

| Key | Env | Default | Accepted | Meaning |
| --- | --- | --- | --- | --- |
| `key_type` | `WORKFLOWS_KEY_TYPE` | bigint | bigint, uuid, ulid | User morph keys (started\_by, published\_by, requested\_by). |
| `owner_key_type` | `WORKFLOWS_OWNER_KEY_TYPE` | \= key\_type | bigint, uuid, ulid | Owner morph keys. |
| `subject_key_type` | `WORKFLOWS_SUBJECT_KEY_TYPE` | \= key\_type | bigint, uuid, ulid | Subject morph keys; every registered subject type must fit it. |
| `primary_key_type` | `WORKFLOWS_PRIMARY_KEY_TYPE` | bigint | bigint, uuid, ulid | The package’s own ids. |
| `models.*` | — | packaged models | a subclass of the packaged model | Swap a model: extend it and name the subclass (definition, version, instance, step\_run, wait, escalation, idempotency\_key, dry\_run). |
| `tables.*` | — | workflow\_\* | ^\[a-z\]\[a-z0-9\_\]{0,39}$ | Table names: definitions, versions, instances, step\_runs, waits, escalations, idempotency\_keys, dry\_runs. |

## Queue, registries and time

| Key | Env | Default | Accepted | Meaning |
| --- | --- | --- | --- | --- |
| `queue.enabled` | `WORKFLOWS_QUEUE_ENABLED` | on | boolean (on/off, true/false, 1/0) | Off = steps run inline. |
| `queue.connection` | `WORKFLOWS_QUEUE_CONNECTION` | app default | string | Where the jobs go. |
| `queue.queue` | `WORKFLOWS_QUEUE_NAME` | app default | string | The queue name. |
| `steps` | — | \[\] | list of StepType class-strings | Host step type classes. |
| `subjects` | — | \[\] | list of DescribesWorkflowSubject class-strings | Subject (and owner) types. |
| `events` | — | \[\] | map | Laravel event → trigger key: a key, '\*' for a WorkflowEvent, or {key, subject, user, payload, idempotency\_key}. |
| `timezone` | `WORKFLOWS_TIMEZONE` | app.timezone | IANA timezone | Default version timezone. |

## Engine, wake and retries

| Key | Env | Default | Accepted | Meaning |
| --- | --- | --- | --- | --- |
| `engine.lease` | — | PT10M | 60 s – 1 day | How long a claimed step may run (job timeout = lease − 30 s). |
| `engine.requeue_after` | — | PT5M | 60 s – 1 day | Re-push a queued step whose job is older than this. |
| `wake.batch` | — | 500 | 1 – 10 000 | Rows claimed per pass (and the for\_each cap per slot). |
| `wake.claim_ttl` | — | PT10M | 60 s – 1 day | A claim older than this reverts. |
| `retry.default.max_attempts` | — | 3 | 1 – max\_attempts\_cap | Default attempts. |
| `retry.default.backoff` | — | \[30, 120, 600\] | list of 0 – 86 400 s | Seconds before attempt 2, 3, …; the last value repeats. |
| `retry.default.jitter` | — | 0.1 | 0.0 – 1.0 | Random spread of each backoff. |
| `retry.max_attempts_cap` | — | 10 | 1 – 100 | Upper bound for any policy. |

## Limits

| Key | Env | Default | Accepted | Meaning |
| --- | --- | --- | --- | --- |
| `limits.max_steps` | — | 200 | 2 – 100 000 | Steps per instance. |
| `limits.max_depth` | — | 3 | 0 – 50 | start\_workflow nesting. |
| `limits.max_starts_per_minute` | `WORKFLOWS_MAX_STARTS_PER_MINUTE` | 60 | 1 – 1 000 000, or off / none / null | Per definition; off = no limit. |
| `limits.max_loop_iterations` | — | 1000 | 1 – 100 000 | loop.max ceiling. |
| `graph.max_nodes` | — | 500 | 2 – 10 000 | Nodes per graph. |
| `graph.max_edges` | — | 2000 | 1 – 100 000 | Edges per graph. |
| `graph.max_bytes` | — | 524 288 (512 KiB) | 1 KiB – 16 MiB | Encoded graph size. |
| `conditions.max_depth` | — | 6 | 1 – 32 | Condition nesting. |
| `conditions.max_clauses` | — | 100 | 1 – 10 000 | Clauses per condition. |
| `steps_output.max_bytes` | — | 65 536 (64 KiB) | 256 B – 16 MiB | Step output cap. |
| `steps_input.max_bytes` | — | 65 536 (64 KiB) | 1 KiB – 1 MiB | A step run’s recorded input cap; larger inputs are stored as {"truncated": true, "bytes": n}. |
| `escalations.max_levels` | — | 5 | 1 – 20 | Levels per node. |
| `idempotency.ttl` | — | P30D | 60 s – 366 days | Trigger key lifetime. |
| `schedule.catch_up` | — | PT1H | 60 s – 1 day | Missed slots coalesced within this window. |

## Authorization, owners, parties and adapters

| Key | Env | Default | Accepted | Meaning |
| --- | --- | --- | --- | --- |
| `authorization.authorizer` | — | GateStepAuthorizer | AuthorizesSteps class-string | The authorization hook. |
| `authorization.abilities` | — | \[\] | map of step type key → ability | Overrides a type’s ability(). |
| `actor.system_user` | — | null | invokable class-string | Returns the service account the system actor authorizes as. |
| `owners.context` | — | teams or none | RestoresOwnerContext class-string | Owner context around every job. |
| `owners.membership` | — | teams or allow | ChecksOwnerMembership class-string | Party membership check. |
| `parties.user_model` | — | auth.providers.users.model | model class-string | The model user parties resolve to. |
| `parties.roles` | — | teams or none | ResolvesRoles class-string | Role party resolver. |
| `adapters.transition` | — | lifecycle bridge | TransitionAdapter class-string | The transition step’s adapter. |
| `adapters.webhooks` | — | null | DeliversWebhooks class-string | Registers the outbound webhook step. |

## Business time, approvals and subjects

| Key | Env | Default | Accepted | Meaning |
| --- | --- | --- | --- | --- |
| `business_time.weekdays` | — | \[1, 2, 3, 4, 5\] | ISO weekdays 1 – 7 | Days of the weekdays calendar (duplicates dropped). |
| `business_time.max_search_days` | — | 366 | 7 – 3660 | Bound of every business-time search the package’s clock does; opening\_hours: calendars use opening-hours.search\_days. |
| `business_time.calendar_owner` | — | null | invokable class-string | Returns the calendar owner of app-wide definitions. |
| `approval.expiry_grace` | — | PT5M | 0 – 1 day | Approval expiry safety-net delay. |
| `subjects_on_deleted` | — | cancel | cancel, keep | What happens to running instances of a deleted subject. |
| `bridges.lifecycle.emit_transitions` | `WORKFLOWS_LIFECYCLE_EMIT_TRANSITIONS` | off | boolean | Emit lifecycle.<lifecycle>.<transition> for every lifecycle state change. |

## Files, dry runs, webhooks, resources and pruning

| Key | Env | Default | Accepted | Meaning |
| --- | --- | --- | --- | --- |
| `sync.path` | `WORKFLOWS_SYNC_PATH` | resources/workflows | absolute, or relative to the base path | Where workflows:sync and workflows:export read and write definition files. |
| `dry_run.log` | `WORKFLOWS_DRY_RUN_LOG` | off | boolean | Log every dry run. |
| `webhooks.enabled` | `WORKFLOWS_WEBHOOKS_ENABLED` | off | boolean | The inbound route. |
| `webhooks.prefix` | — | workflows | string (slashes trimmed) | Route prefix. |
| `webhooks.middleware` | — | \['api'\] | list of middleware | Route middleware. |
| `webhooks.verify_middleware` | — | \[\] | list of middleware | Required when enabled: the signature check; empty keeps the route unregistered. |
| `webhooks.max_bytes` | — | 262 144 (256 KiB) | 256 B – 16 MiB | Request body cap (413 above it). |
| `resources.owner / subject / user` | — | packaged reference resources | instantiable JsonResource subclass | Swap with your own to add a name or an avatar. |
| `prune.instances_after` | — | P90D | 1 hour – 10 years | Retention of ended instances. |
| `prune.dry_runs_after` | — | P30D | 1 hour – 10 years | Retention of dry-run logs. |

## Environment

```bash
WORKFLOWS_KEY_TYPE=bigint
WORKFLOWS_OWNER_KEY_TYPE=
WORKFLOWS_SUBJECT_KEY_TYPE=
WORKFLOWS_PRIMARY_KEY_TYPE=bigint
WORKFLOWS_QUEUE_ENABLED=true
WORKFLOWS_QUEUE_CONNECTION=
WORKFLOWS_QUEUE_NAME=
WORKFLOWS_TIMEZONE=
WORKFLOWS_MAX_STARTS_PER_MINUTE=60
WORKFLOWS_LIFECYCLE_EMIT_TRANSITIONS=off
WORKFLOWS_SYNC_PATH=resources/workflows
WORKFLOWS_DRY_RUN_LOG=off
WORKFLOWS_WEBHOOKS_ENABLED=off
```

## 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)
