Durable Workflows in PHP

Sagas, orchestrators, chained workflows, outbox, and #[Delayed] saga timeouts — on the database and broker you already operate. No separate workflow service. Plain PHP classes with attributes.

Composer package · Laravel, Symfony, or Tempest · PostgreSQL or MySQL · RabbitMQ, Kafka, SQS, Redis, or DBAL outbox

How it compares

What Ecotone adds over a single-purpose tool

Each tool below is a capable choice for its slice. Ecotone covers the same ground and adds the operational layer around it — so aggregates, projections, sagas, and messaging share one model and one set of guarantees.

Temporal PHP SDK

Best-in-class durable execution, on its own runtime.

Where Ecotone goes further than this library

  • Ecotone delivers durable workflows on the database and broker you already operate — no separate cluster, no RoadRunner, no ext-grpc.
  • Workflows are plain PHP classes with attributes, with no deterministic-replay constraints — Date::now, randomness, and direct I/O are all fine.
  • Your existing RabbitMQ / Kafka / SQS carry the messages directly, rather than through a separate engine's internal task queues.
  • Saga state and event streams live in your own PostgreSQL or MySQL — your backups, your retention, queryable for new projections.
Durable Workflow (formerly Laravel Workflow)

A focused Laravel-native durable-workflow engine.

Where Ecotone goes further than this library

  • Ecotone delivers the same durable-workflow surface — sagas, orchestrators, chained handlers, #[Delayed] timeouts — on Laravel or Symfony.
  • #[Saga] adds an identifier-mapped process manager that lives across events arriving over time.
  • The same model also covers CQRS, event sourcing, the outbox, and distributed messaging, so the pieces around the workflow share one set of guarantees.
  • A Distributed Bus and Service Map extend workflows across services and brokers when you need it.
Symfony Messenger alone

A mature message bus and transport layer.

Where Ecotone goes further than this library

  • Ecotone adds a first-class #[Saga] process manager and stateless chained workflows on top of the bus.
  • #[Delayed] gives event handlers timeout/wait semantics (TimeSpan, DateTime, or expression) via the broker's delayed-message primitive — no custom scheduler.
  • Per-handler failure isolation: each subscriber gets its own copy of the message, so a retry stays scoped to the handler that failed.
  • Runs on Symfony Messenger transports directly, so existing transport investment carries over.
Laravel Queues / Horizon

A robust job runner with strong operational tooling.

Where Ecotone goes further than this library

  • Ecotone adds an identifier-mapped #[Saga] coordinator that persists state across events over time, on the same Laravel queues.
  • Multi-step durability via the outbox (CombinedMessageChannel): each step's message is committed to the database with its business write, then handled by consuming from the broker.
  • #[Delayed] timeouts, orchestrators, and chained handlers provide the workflow shapes on top of the queue.
  • Runs on the Laravel queues and Horizon you already operate.
Ecotone

Durable execution on your own database — three workflow shapes, one model.

  • Crash survival on your own infrastructure. Workers can be killed mid-flow; the broker holds the message until the next consumer picks it up. Deploys are no different from any rolling deploy of your app — no special workflow-version migration, no engine state to coordinate.
  • Three workflow shapes, one declarative model. #[Saga] for stateful processes that react to events over time. #[Orchestrator] for declarative routing-slip workflows whose step list is visible at one glance. Chained #[InternalHandler] for stateless flows where the message carries the state.
  • Timeouts as attribute, not infrastructure. #[Delayed] resumes a handler after a TimeSpan, an exact DateTime, or an expression — the broker's delayed-message primitive does the waiting. No cron, no separate timer service.
  • Per-handler failure isolation by default. Each subscriber gets its own copy of the message, so one failing handler never triggers sibling re-runs on retry. The failure of one step never replays a side effect on another.
  • Cross-service workflows on the same primitives. #[Distributed] handlers and the Distributed Bus extend the workflow across bounded contexts on the brokers you already operate — the retry / DLQ / idempotency / outbox guarantees apply uniformly inside the saga and across service boundaries.
  • Data stays in your database. Saga state, outbox, dead-letter, projection cursors, event streams — all in your PostgreSQL or MySQL. Your backups, your retention, your access controls. New subscribers and projections can be added against the same event stream directly.

Frequently asked questions

Haven’t found what you’re looking for? Contact us

Sagas persist state per identifier in your own database; CombinedMessageChannel commits the business state change and the outgoing message together in one DBAL transaction; the broker redelivers on crash and the saga reloads from the DB by identifier; built-in deduplication tolerates the duplicate. Saga timeouts are a #[Delayed] attribute on an event handler — TimeSpan, exact DateTime, or expression — handled by the broker's delayed-message primitive. Orchestrators declare multi-step workflows with routing-slip semantics. Three workflow shapes (saga, orchestrator, chained handlers), one attribute-driven model, on the database and broker you already operate.
Saga timeouts are a single #[Delayed] attribute on an event handler. The handler fires after the specified TimeSpan, exact DateTime, or expression — no cron, no separate timer service. The broker's underlying delayed-message primitive (RabbitMQ delayed exchange, SQS message timer, Redis sorted-set) does the waiting; the consistent attribute model hides the broker specifics from your code.
The broker redelivers the message. The saga reloads from the database on the next event arrival bearing its identifier. The handler runs again — built-in deduplication tolerates duplicate delivery, and the outbox ensures the business write and the message commit together. Crash recovery is broker redelivery against state in your own database — no engine-shaped event history, no external runtime to coordinate with. Workers can be killed mid-flow; deploys are no different from any rolling deploy of your app.
Yes. Chained handlers via outputChannelName give stateless durable workflows where the message itself carries the state. When the async channel runs through an outbox (CombinedMessageChannel), each step is committed atomically with its business write before the message advances. If a worker crashes, the broker redelivers; the work resumes on the next consumer. No saga record needed.

Be part of the change with EcotoneCurve

Unleash the power of Messaging in PHP
and push productivity to the higher level

Get started
Gradient
Shapes 1
Shapes 2
DiscordTwitterSupport and ContactTelegram