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.
Release reviewed: v2.0.0 (breaking; follows v1.0.0 by hours). Net verdict: the risk profile improved materially, but the integration plan changes — two prior blockers were cleared (no out-of-box storage adapter; resume() unusable on failure), one prior "strength" was downgraded (condition/state rigor was not actually enforced), and the timeout/retry gap is now confirmed permanent rather than pending.
Strengths:
Storage\InMemoryStorage now ships in the package (previously in tests/ under autoload-dev, so the library could not execute a workflow on install). This removes a real adoption blocker and makes the pilot phase single-day rather than single-sprint.true for a 500-dollar order against 'order.total > 1000 && order.vip === true'.FAILED is no longer terminal; FAILED → RUNNING is legal and resume() retries the failed step. This aligns far better with human-in-the-loop and remediation flows than an immutable failed state.WAITING, distinguishable from an executing workflow. This is an operational-correctness win, not a cosmetic one.Event, Queue, and Mail.Challenges (revised):
completed at 66.67%, resume() masking its own error, start() overwriting an existing instance, DelayAction(hours: 2) pausing one second. All are fixed with regression coverage, but the pattern tells us engine-reported status cannot be trusted as a business-outcome signal without our own assertions.EmailAction → FakeEmailAction, WorkflowBuilder::email() → fakeEmail(), ConditionAction drops on_true/on_false, condition grammar moves from =/is to ===/==/!=, and FAILED is no longer terminal.Support\Arr::has() is new, suggesting internal helper consolidation, but removal of data_get()/data_set() is not stated in the release notes. Verify before relying on any non-Laravel execution path.QueueExecutor/QueuedWorkflowExecutor, DatabaseStorageAdapter, a real mail action, and any timeout/retry middleware.HttpAction now reports missing ext-curl as a step failure rather than a fatal error — confirm curl is present in our base images so we fail cleanly rather than at step level.EmailAction, email(, on_true/on_false, and condition strings using =/is; identify handlers and dashboards that assert FAILED is terminal; identify any workflow whose passing result depended on the old silent string-comparison behaviour (these will now throw at parse time — desirable, but they surface as deploy-day failures).InMemoryStorage + SimpleWorkflow for sync, non-critical flows; replace data_get() if we need framework independence; add acceptance tests asserting business outcomes, not engine status.DatabaseStorageAdapter, QueuedWorkflowExecutor, in-house timeout/retry middleware, WAITING-aware status mapping, Laravel Event wiring for failure notification.CacheStorageAdapter, real MailAction via Laravel Mail/Notification.composer validate --strict clean.2.0.0, not ^2.0).WorkflowEngine in a service provider; add InMemoryStorage for tests.DatabaseStorageAdapter → QueuedWorkflowExecutor.WorkflowFailedEvent → alerting), then WAITING-aware dashboards.resume() errors, instance overwrite on start(), and mutual-reachability/cyclic definitions (now rejected at parse time) are all closed with tests.false (previously null → 0 made missing.key < 1000 true) — removes a class of false-positive approvals.FakeEmailAction naming removes the worst kind of defect: an action that reported 'status' => 'sent' and sent nothing.FAILED is now mutable — any code, report, or alert asserting failed workflows are immutable will silently misreport.DelayAction now actually sleeps the documented duration — a delay(hours: 2) that previously returned in one second now blocks for two hours. Any delay used inside a synchronous/web request path becomes a hang.EmailAction/email() anywhere, and what is our real mail path? (Impact: must implement a WorkflowAction on Laravel Mail.)FAILED is terminal (handlers, dashboards, alert rules)? (Impact: FAILED → RUNNING is now legal.)WAITING? (Impact: unmapped state reads as stuck or as running.)data_get()/data_set() actually been replaced by Support\Arr? (Impact: determines whether we can ever run core outside Laravel.)ext-curl guaranteed in every runtime image? (Impact: step failures vs fatals.)| Component | Laravel Equivalent | Strategy (post-2.0.0) |
|---|---|---|
| Storage | Database, Cache, Redis |
InMemoryStorage ships for dev/test; build DatabaseStorageAdapter for prod |
| Async execution | Queue |
Build QueuedWorkflowExecutor; bound timeouts at the job layer |
| Retry / timeout | Queue retries, retry(), job $timeout |
Own it — engine explicitly does not perform these |
| Events | Event / Broadcasting |
Dispatch on WAITING, FAILED, and FAILED → RUNNING recovery |
Mail / Notification |
Replace FakeEmailAction with a real action |
|
| Logging | Log facade |
LaravelLoggerAdapter |
| Config | config() / .env |
Bind engine in a service provider |
| Testing | Pest, DatabaseMigrations |
InMemoryStorage for unit; DB adapter for integration; outcome assertions |
2.0.0; execute the breaking-change audit; stand up InMemoryStorage; ship one non-critical sync workflow end to end with outcome assertions.DatabaseStorageAdapter; QueuedWorkflowExecutor; timeout/retry middleware; failure alerting via events; WAITING-aware operational views.NotifyAction), removal of any remaining Illuminate helper coupling.Support\Arr suggests reduced framework coupling; SECURITY.md now provides a disclosure path for vendor risk review.data_get() status unconfirmed.InMemoryStorage in test suite.DatabaseStorageAdapter.QueuedWorkflowExecutor + job-level timeout/retry.WAITING handling.WAITING gives support a truthful "blocked on prerequisite" signal; getStatusSummary() plus rich events support triage; FakeEmailAction removes a class of "we told the user it was sent" incidents; SECURITY.md gives a defined disclosure path.FAILED being recoverable means support scripts and runbooks must change; failures previously hidden by the engine's own error handler now surface (good, but expect a transient ticket bump during rollout); no Laravel integration guides upstream.InMemoryStorage remains fine for admin/CLI paths only; durable throughput still requires our DatabaseStorageAdapter + QueuedWorkflowExecutor.DelayAction now genuinely sleeps; any long delay in a synchronous path is a scaling hazard and must move to queued execution.start() overwriting state/history, cycles accepted, missing-key comparisons coercing to true, delay silently collapsing to one second, missing ext-curl as a fatal.FAILED is terminal or that only knows PENDING/RUNNING/COMPLETED.DelayAction honouring real durations can block request threads; unmapped WAITING can read as "stuck" in monitoring.FAILED semantics) before feature work.How can I help you explore Laravel packages today?