solution-forest/workflow-engine-core
A framework-agnostic PHP 8.3+ workflow engine for defining, executing, and tracking workflows with state management, retries, timeouts, and conditions. Extensible and type-safe, with no framework dependencies.
Build vs. Buy — still "buy," but scope the buy correctly. Purchase the engine logic (condition evaluation, state machine, transition validation), not the integration. v2.0.0 ships Storage\InMemoryStorage so the library finally runs out of the box, but that is a test-grade adapter. There is still no database or Redis storage adapter. On Laravel, someone must own an Eloquent/Redis adapter as first-party work. Budget it as a line item, not an afterthought.
Feature expansion — branching is now real. ConditionEvaluator previously had no &&/|| support and string-compared the operator into the right-hand side, so 'order.total > 1000 && order.vip === true' evaluated true for a $500 order. Boolean operators, parentheses and negated groups are now supported, and malformed expressions throw instead of collapsing to a boolean. This is the difference between "can model an approval chain" and "cannot." Tiered pricing, fraud gates, escalation rules and multi-criteria routing are now legitimately on the table.
Roadmap alignment — the version risk profile has changed shape, not disappeared. The prior assessment flagged "pre-v1.0, actively under development" as the adoption blocker. That is now stale: v2.0.0 is a semver-stable tag on PHP 8.3–8.5. It has been replaced by a sharper risk: v1.0.0 was tagged stable earlier the same day while carrying defects that made the engine silently do the wrong thing and report success. Plan for a pinned version and a review gate on upgrades rather than a blanket "wait for 1.0."
Auditability and compliance — improvement, with a caveat worth stating out loud. WAITING now distinguishes workflows parked on unmet prerequisites from ones actively executing; start() no longer discards an existing instance's state and history; and resume() on a failed workflow now retries the failed step instead of masking its own error inside the failure handler. Those are the primitives an audit trail needs. The caveat: an audit trail is only worth what the engine's correctness is worth, and v1.0.0 demonstrated silent wrong answers with a success status. Treat compliance claims as "supported by the model, contingent on engine correctness," and re-verify after each upgrade.
Use cases, with corrected expectations:
completed at 66.67% progress, so parallel post-payment branches are now trustworthy.DelayAction now honours the documented minutes/hours keys (delay(hours: 2) previously paused for one second). Any cooling-off period, probation window or reminder cadence was silently broken before this release.HttpAction now reports missing ext-curl as a step failure rather than a fatal error, which matters for third-party callouts.Cost and scope corrections from the breaking changes. If anything was prototyped on v0.0.4-alpha or v1.0.0, budget migration:
EmailAction → FakeEmailAction, and WorkflowBuilder::email() → fakeEmail(). The old action never sent mail but reported 'status' => 'sent'. Any PoC that appeared to send notifications did not. Real mail transport is new work.ConditionAction drops on_true/on_false (read but never consumed). Branch with a condition on the transition instead.===/==/!=, not =/is.FAILED is no longer terminal. Any code asserting failed workflows are immutable must be updated — which is also the fix that makes resume work at all.Retries, timeouts and compensation are NOT features of this package. The v1.0.0 documentation claimed them; v2.0.0 corrected the docs because the engine does not perform them. If the roadmap assumed resilience was included, that assumption is now a build item or a reason to look at Temporal.
✅ PHP 8.3–8.5 project needing an embeddable workflow engine without operating Temporal, Camunda or Zeebe.
✅ Branching logic is the core requirement — approval chains, risk gates, tiered routing. The condition grammar is now correct and fails loudly on malformed input.
✅ You will own the persistence adapter. Accept that InMemoryStorage is for tests and that a DB/Redis adapter is your build.
✅ Long-running, stateful processes where distinguishing "parked on a prerequisite" from "actively executing" matters operationally.
✅ Framework-agnostic core is acceptable — Laravel works, but there is no service provider or Eloquent storage shipped; you are integrating, not installing.
✅ The team can pin versions and review upgrades. Consume with a committed composer.lock and read the changelog before bumping.
✅ Multi-branch workflows — the multi-root branch-dropping defect is fixed and regression-covered.
❌ You need production persistence out of the box. Only InMemoryStorage ships. For a Laravel team this is a non-trivial, undifferentiated build.
❌ You need retries, timeouts or compensation/rollback actions. Documented as absent. Do not plan around them.
❌ You need real side effects (email, HTTP with retry semantics) without writing custom WorkflowAction implementations.
❌ Regulated or high-blast-radius domain and you need a proven correctness record. v1.0.0 shipped a stable tag with silent-wrong-answer defects the same day it was cut. The response since has been strong — 161 tests / 363 assertions with regression coverage for every issue, CI now running on pull requests rather than only pushes — but "silent success on a wrong result" is precisely the failure mode a workflow engine cannot afford. Wait one clean stable cycle, or gate adoption behind a spike with your own assertion suite.
❌ You rely on dependency automation. The release notes confirm PRs previously did not run the suite, so dependabot auto-merge could land untested changes on main. If your upgrade path is automated, this package's history is a reason to add a manual gate.
❌ GUI workflow designers, sub-millisecond latency, serverless/edge — unchanged from the prior assessment.
"We evaluated this PHP workflow engine for automating multi-step processes like order fulfillment and employee onboarding. The v2.0.0 release is a meaningful step: it fixes a class of bug where the engine reported success while producing the wrong result — for example, a pricing condition that approved a $500 order against a $1,000 threshold, and a delay step that paused one second instead of two hours. Those defects are now fixed, with regression tests for each, and the CI pipeline now runs on every pull request rather than only on pushes to main."
"Three things to weigh:
Recommendation: a scoped proof-of-concept on one high-value workflow with a pinned version and our own test coverage, not a broad rollout. The build-from-scratch alternative still costs six to twelve months; this remains the cheaper path, provided we go in with accurate expectations."
Drop the previous "handles 10 or 10,000 concurrent workflows equally well" claim — it was never substantiated, and this release gives no throughput evidence either way.
"v2.0.0 is a correctness release, not a feature release. What is now true that wasn't:
&&, ||, parentheses and negated groups; malformed expressions throw rather than silently coercing to true. Note the grammar change — ===/==/!=, not =/is.FAILED is no longer terminal; resume() retries the failed step and no longer swallows the underlying cause in its own error handler.completed at partial progress.start() no longer clobbers an existing instance, its state and its history.WAITING rather than masquerading as RUNNING.DelayAction actually reads minutes/hours; HttpAction degrades to a step failure when ext-curl is absent; relational comparisons against a missing key are false (previously null coerced to 0, so missing.key < 1000 was true).Storage\InMemoryStorage now ships in the package rather than living under autoload-dev in tests/.What is explicitly not there: retries, timeouts and compensation actions. The docs previously claimed them; v2.0.0 corrected the docs because the engine does not perform them. Plan accordingly.
Migration is mechanical but real: rename email() → fakeEmail() and EmailAction → FakeEmailAction (and implement a real mail transport, because the old action reported sent without sending anything), move on_true/on_false branching to a condition on the transition, and update anything asserting failed workflows are immutable.
Verification posture is good: 161 tests / 363 assertions, green on 8.3, 8.4 and 8.5; Pint, PHPStan level 6 and composer validate --strict clean; CI now runs on PRs. Pin the version and review each bump. Let's spike one workflow and write our own integration tests around it — given the v1.0.0 history, our test coverage is the real safety net, not theirs."
"The fluent API is unchanged in shape, but the semantics are stricter and the docs are finally honest:
WorkflowBuilder::create('order-processing')
->addStep('validate', ValidateOrderAction::class)
->when('order.total > 1000 && order.vip === true', fn($b) => $b->addStep('fraud_check'))
->addStep('fulfillment', ShipOrderAction::class)
->build();
Note the compound condition — that exact expression would previously have matched a $500 non-VIP order. It now parses properly and throws on malformed input instead of guessing.
Also new: WorkflowDefinition::getFirstSteps() and Support\Arr::has(). InMemoryStorage ships in the package, so you can run a workflow in a test without pulling from tests/. FAILED is a resumable state now, so retry-on-failure is on you but the state machine will let you do it.
Two things to internalise before you write code: fakeEmail() is a fake and always was — if you need mail, implement WorkflowAction with a real transport. And do not build on retries or timeouts; the engine doesn't do them, whatever an older doc said. SECURITY.md documents the disclosure process and scope limits — read it before you put this in front of customer data."
How can I help you explore Laravel packages today?