Skip to content

Workflow examples — arqel-dev/workflow ​

This folder contains three complete and annotated state machine examples built with the arqel-dev/workflow package. Each one was chosen to exercise a distinct subset of the package's features; together they cover ~all the design decisions that show up in real Laravel apps.

Use these examples as a starting point when you design your own workflow: copy the structure, adapt states/transitions to your domain, and remove what you don't use.

The three examples ​

ExampleDomainMain focus
order-states.mdE-commerce — ordersRole-based authorization (authorizeFor + Gate), carrier webhooks, "any-to" transition (Cancelled), idempotency via metadata
article-states.mdCMS editorial — articlesHuman collaborative flow, rejection with feedback, 100% Gate-based authorization, integration with arqel-dev/versioning for content snapshots
subscription-states.mdSaaS billing — subscriptionsTransitions triggered by Stripe webhooks, side-effects in cache/quotas, system as actor (humans have limited access), heavy use of metadata for auditing

Feature comparison table ​

arqel-dev/workflow featureorderarticlesubscription
WorkflowDefinition + HasWorkflow traityesyesyes
StateTransitionField on the Resourceyesyesyes
StateFilter on the Tableyesyesyes
Authorization via authorizeFor()yes—yes
Authorization via Gate (transition-X-to-Y)yesyes—
Listener for StateTransitioned (queued)yesyesyes
"any-to-X" transition (multiple from())yes (Cancelled)—yes (Canceled)
Side-effects in side-systems (email/cache/queue)emailemail + jobscache + quotas + email + metrics
Use of metadata in historytracking_code, webhook_event_idfeedbackwebhook_event_id, event_type, recovery
Idempotency via metadata->webhook_event_idyes—yes (central)
System as actor (user null)partial (webhook delivery)—yes (Stripe webhooks)
Terminal state with no return—yes (Archived)yes (Canceled)
Integration with arqel-dev/versioning—yes—
defaultFilters on the Table—yesyes
Domain validation in the Gate (filled(...))yes (refund_reason)——

Cross-cutting patterns worth highlighting ​

Slugifying transitions. TransitionAuthorizer derives the Gate name as transition-{from-slug}-to-{to-slug}, where slug is the last part of the state's FQCN, without the State suffix, in kebab-case. OrderState\Pending → pending, ArticleState\InReview → in-review, SubscriptionState\PastDue → past-due.

Single vs multiple listeners. The article example uses a single listener with match($event->to) for the three relevant cases — simple and readable for lightweight handlers. subscription does the same (one listener for all transitions) because the side-effects share services (FeatureFlagCache, QuotaManager). order, on the other hand, splits into distinct listeners (NotifyCustomerOfShipment, etc.) because each has very different dependencies and criticality levels. There's no hard rule — pick whichever stays most readable for your team.

State vs Action. When you find yourself tempted to add a Archived → Draft or Canceled → Active transition, stop. The rule is: a state machine represents the resource's natural lifecycle. Operations that "resurrect" a terminal resource should be Actions that create a new record (duplicate article, create new subscription) — preserving the old one's history.

Metadata as a contract. In all three examples the transition's metadata follows an implicit schema: fields like webhook_event_id, subscription_id, feedback, tracking_code recur. Document that schema in your app's SKILL.md — auditors and future developers will thank you.

How to use these examples ​

  1. Read the example whose domain most resembles yours.
  2. Copy the model + workflow definition + transition classes into your app, adapting names.
  3. Decide your authorization strategy (Gate, authorizeFor, or mix) by reading the table above.
  4. Write listeners for the relevant side-effects — start with a single match() listener if they're simple.
  5. Add StateTransitionField to the Resource and StateFilter to the Table.
  6. Write Pest 3 tests for each transition (see packages/workflow/tests/Feature in the monorepo for patterns).

See also ​

MIT License — built with Inertia + React + Laravel.