# durable-workflow/workflow: durable execution inside a Laravel application

> Durable Workflow is a PHP package that lets you write long-running workflows as ordinary Laravel code and keeps their execution history in the database, so a worker restart or a deploy does not lose completed work. The same history model comes with a v1 to v2 migration switch, two timeout budgets for schedules, and three deployment shapes.

**durable-workflow/workflow** — Core package for defining and running durable workflows and activities. Supports long-running persistent workflows, retries, queues, parallel execution, workflow monitoring, dedicated storage connections, and orchestration for microservices, data pipelines, sagas, agentic workflows, and other complex business processes.

- Repository: https://github.com/durable-workflow/workflow
- Website: https://durable-workflow.com
- Stars: 1,253 · Forks: 86
- Language: PHP
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/durable-workflow-workflow

## The runtime is the application you already have

Durable Workflow is described as the embedded Laravel runtime for durable execution, and the framing is deliberate. You write long-running workflows as ordinary PHP, completed work is recorded through worker restarts and application deploys, and the runtime that executes them is Laravel's own queues, cache and database.

That is the whole architectural claim, and it is a different bargain from the usual workflow engine. There is no separate cluster to run, no control plane to operate, and no proprietary definition format, because a workflow is a PHP class in your codebase and the history is in your database. The cost is equally clear: the durability guarantee is only as good as your queue, your database and your deployment practice, and scaling the execution means scaling the Laravel application.

The package also ships the orchestration engine that is hosted by Durable Workflow Server, and the PHP SDK lets you point workers at Server or at Durable Workflow Cloud when the workers need to run outside the Laravel application or across PHP, Python and Rust. So the same workflow definitions can graduate from embedded to a separate runtime, which is the migration path the deployment table at the end of the README is really describing.

The project is MIT licensed, the site is durable-workflow.com, and the topic list covers the ground the package is aimed at: durable execution, orchestration, sagas, queues, background jobs, BPM, BPMN, Laravel, agentic workflows and workflow automation.

## A workflow is a class, an activity is a class

The first workflow example is short enough to take in at once, and it shows the shape of the API.

```php
<?php

use Workflow\V2\Activity;
use Workflow\V2\Workflow;
use Workflow\V2\WorkflowStub;
use function Workflow\V2\activity;

final class GreetActivity extends Activity
{
    public function handle(string $name): string
    {
        return "Hello, {$name}!";
    }
}

final class GreetWorkflow extends Workflow
{
    public function handle(string $name): string
    {
        return activity(GreetActivity::class, $name);
    }
}

$workflow = WorkflowStub::make(GreetWorkflow::class);
$workflow->start('world');

echo $workflow->output(); // Hello, world!
```

Three classes do the work. An Activity is a unit of work with a handle method that takes arguments and returns a result. A Workflow is the orchestration, and its handle method coordinates activities. The WorkflowStub is the handle you hold onto from the outside, and it is what you start and what you read the output from.

The namespace is Workflow\V2, and that version prefix is the visible edge of the v1 to v2 migration described below. The activity function is imported as a function, not called as a static, which keeps the workflow body readable as a sequence of steps.

The output call is worth a second look. It returns the result of the workflow synchronously in the example, which is convenient for a fast workflow, but the underlying model is asynchronous: a workflow that runs for an hour returns a stub, and you ask the stub for its output when it is done. The persistence layer underneath is what makes that safe.

## What the execution history buys you

The README lists what workflow code can coordinate: activities, timers, signals, queries, updates, child workflows, sagas, cancellation, retries, parallel work, side effects, continue-as-new, search attributes, memo, and message streams.

The mechanism that makes all of this work is stated in one sentence: the runtime persists execution history so replay can resume after process or host failure without repeating completed activities.

That is the durable execution model in full. Every step is recorded, and when a worker dies mid-workflow, the next worker replays the history, sees what already happened, and continues from there rather than starting over. The activity that already succeeded does not run a second time, which matters enormously when the activity charged a card, sent an email or wrote to a third party system.

The vocabulary around it is worth learning because it maps to a specific mechanism each time. A timer pauses the workflow for a duration rather than blocking a worker. A signal delivers information to a running workflow, and a query reads state from one without changing it, while an update is a query that does change something. A child workflow is a workflow called from another workflow, which is how you compose. A saga is the pattern for a distributed transaction across services, with compensation steps when a later step fails. Side effects record an external action in the history so it is not repeated on replay. Continue-as-new starts a fresh execution with a new history when the current one grows large, and search attributes are the indexed fields that let you find a workflow later. A memo is a cached value stored alongside the history.

The important caveat is that all of this is Laravel's own infrastructure doing the work. Your queue is the dispatcher, your database is the history store, and the retry and timeout behaviour is bounded by what your deployment can promise.

## The v1 to v2 switch, and why the default is not false

Installation is two commands:

```bash
composer require durable-workflow/workflow:^2.0
php artisan migrate
```

Then you need a worker to execute anything:

```bash
php artisan queue:work
```

The migration is the part with a decision in it. For a new v2-only application you set DW_V1_ENABLED=false before the first migration, and that omits the legacy v1 tables and the queue watchdog. The default is the opposite, because existing applications keep v1 enabled so they can drain older work, and the documentation says not to disable it until that work is retired.

The reassurance is that changing the setting does not drop existing tables, so the decision is reversible in the safe direction. The bad outcome to avoid is turning off the v1 path while v1 work is still queued, because that work has nowhere to drain to.

One more runtime note concerns the default database queue on SQLite. The README says failed reservation writes preserve unclaimed work after a proven rollback, and points at a document about SQLite queue reservations for the supported framework versions and the custom-connector boundary. That is a narrow, technical caveat, but it is the kind of narrow caveat that matters if you are running workflows on SQLite in production.

The queue worker is the other half of the operational picture. A durable workflow that nobody is working on makes no progress, so the worker, or Horizon, is a piece of infrastructure you now depend on. The watchdog that the v1 flag controls is exactly the kind of thing that exists because that dependency is real.

## Two timeout budgets for scheduled work

Scheduling is where the timeout semantics get subtle, and the package names both limits explicitly.

```php
use Workflow\V2\Support\ScheduleManager;

$schedule = ScheduleManager::create(
    scheduleId: 'daily-greeting',
    workflowClass: GreetWorkflow::class,
    cronExpression: '0 9 * * *',
    arguments: ['world'],
    executionTimeoutSeconds: 120,
    runTimeoutSeconds: 60,
);
```

You give a schedule an id, a workflow class, a cron expression, the arguments, and the two timeouts. The scheduler entry point is the artisan command workflow:v2:schedule-tick, run from Laravel's scheduler, and it starts the occurrences that are due. The trigger method on ScheduleManager uses the same limits.

The distinction between the two timeouts is the point. The execution timeout covers one logical execution across retries and continue-as-new, so it is the budget for the whole business process no matter how many times it is retried or restarted. The run timeout limits each individual run and resets on continue-as-new, so it bounds a single attempt rather than the process. Each scheduled occurrence starts a new execution budget.

Omit either option, or pass null, and that limit is left unset, which is worth knowing before you assume a default is protecting you.

The documentation for the action format, validation and updates is in docs/schedules.md, and the example above is a daily nine o'clock greeting with a two minute execution budget and a one minute per run budget, which is the shape most scheduled jobs will take.

## Three deployment shapes and what moves when you change

The deployment table is the part to read first, because it decides how much you own.

Embedded Laravel is for workflows and activities that live inside one Laravel application, and your application owns persistence and queue execution. Self-hosted Server is for workers that run independently or in multiple languages, and your team operates Server, MySQL, Redis and optionally Waterline. Durable Workflow Cloud is a managed runtime for PHP, Python and Rust workers, where Durable Workflow operates the runtime and the persistence.

The line between them is ownership, and it is the line you should draw your decision along. Embedded is the least operational work and the most coupling to your application. Server gives you language flexibility and separate scaling, and it brings a MySQL and Redis to run. Cloud removes the operations and adds a vendor.

The migration note matters if you are planning a move: embedded runs remain owned by the Laravel application, and moving new work to Server or Cloud does not reinterpret existing embedded history. In other words, the two kinds of execution coexist, and the histories stay as they were written.

Two supporting pieces complete the picture. The PHP SDK is what lets workers talk to Server or Cloud, and the monitoring guide covers Waterline, which the self-hosted row lists as optional. There is also a runnable sample app in a separate repository, and an embedded HTTP example in the examples/ directory of this one.

The versioned documentation is organised by mode, with separate pages for embedded installation, embedded feature guides, the configuration reference, deployment modes and monitoring, all under the 2.0 path.

## The repository, the tooling and the release pace

The release cadence is fast. Version 2.5.20 was tagged on 2026-10-14, 2.5.21 on 2026-10-15 in the early hours, and 2.5.22 the same day, with the last push on 2026-10-18 as well. The repository is not archived. A patch numbering scheme on a 2.5.x line with several releases in two days is a project that ships fixes quickly, and it also means you should pin the version you have tested rather than tracking the latest.

The repository is set up like a serious PHP package. There is a phpstan.neon for static analysis, an ecs.php for coding standards, a phpunit.xml for tests, a testbench.yaml and a workbench/ directory for the package test harness, and a scripts/ directory. A regression-corpus-policy.json is present, which suggests the project keeps a corpus of regression cases and has a policy for it. The examples/ directory holds a runnable embedded HTTP example, and docs/ holds the reference material, including the schedules document and the SQLite queue reservations note.

There is a CHANGELOG.md at the root, an AGENTS.md, a CONTRIBUTING.md and a.devcontainer/ directory, so the project is set up for agent-driven contributions and reproducible local development.

The licence is MIT, which is the permissive choice you want for a package that becomes part of your application. The project is sustained by contributors and sponsors, with Freispace Resource Scheduling and Translate a Book listed, and there is a GitHub Discussions board and a Discord server for questions and design discussions.

## Conclusion

Adopt durable-workflow/workflow if you are running Laravel and have multi-step work that outlives a request, a queue job or a deploy, and you want the orchestration to live in the same application and the same database as the rest of your code. Do not adopt it if your work fits in a single queued job, since the durable execution model adds a history table and a replay model you do not need there, and do not disable the v1 compatibility path while old work is still queued. Before you start, run php artisan migrate, decide deliberately whether DW_V1_ENABLED should be false in a new application, keep a queue worker or Horizon running with php artisan queue:work, and read the deployment modes page before you plan to move execution off the application.

## FAQ

### What is durable-workflow/workflow for Laravel?

It is an embedded Laravel runtime for durable execution. You write long-running workflows as ordinary PHP classes, and the runtime persists the execution history so replay can resume after a process or host failure without repeating completed activities, using Laravel's queues, cache and database as the runtime.

### How do I install durable-workflow and run my first workflow?

Run composer require durable-workflow/workflow:^2.0 followed by php artisan migrate, then start a worker with php artisan queue:work or Horizon. A workflow is a class extending Workflow, an activity is a class extending Activity, and you start it through a WorkflowStub.

### What does DW_V1_ENABLED control?

Setting it to false before the first migration omits the legacy v1 tables and the queue watchdog. Existing applications keep v1 enabled by default so they can drain older work, and the documentation says not to disable it until that work is retired. Changing the setting does not drop existing tables.

### What is the difference between executionTimeoutSeconds and runTimeoutSeconds?

The execution timeout covers one logical execution across retries and continue-as-new, while the run timeout limits each individual run and resets on continue-as-new. Each scheduled occurrence starts a new execution budget, and omitting either option leaves that limit unset.

### Can durable-workflow workers run outside Laravel?

Yes. The package also provides the orchestration engine hosted by Durable Workflow Server, and the PHP SDK lets workers point at Server or Durable Workflow Cloud when they need to run outside the Laravel application or across PHP, Python and Rust. Moving new work does not reinterpret existing embedded history.

## Sources

- [durable-workflow/workflow on GitHub](https://github.com/durable-workflow/workflow)
- [License: MIT](https://github.com/durable-workflow/workflow/blob/main/LICENSE)
- [Project website](https://durable-workflow.com)
- [README](https://github.com/durable-workflow/workflow/blob/main/README.md)
- [Releases](https://github.com/durable-workflow/workflow/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/durable-workflow-workflow
