Sample App
https://github.com/durable-workflow/sample-app
This is a sample Laravel 13 application built on Durable Workflow 2.0 (alpha) with example workflows that you can run inside a GitHub codespace.
The sample app is the recommended starting point for "how do I learn Durable Workflow?". It is the first place new features get end-to-end coverage, the first place bugs are reproduced against a realistic Laravel host, and the source the docs site mirrors when it shows what a pattern looks like in practice. If you have a real durable-workflow pattern you want to share, the Contribute a Sample guide walks through the submission flow.
Looking for the Laravel 12 / Durable Workflow 1.x version? It's preserved on the
Laravel-12branch. Older blog posts and tutorials that reference v1 patterns (e.g.Workflow\Workflow,yield activity(...),Workflow\Activity) target that branch.
Sample gallery
Each entry in the gallery names the pattern surface the sample teaches, the workflow class that exercises it, the artisan command that runs it, and the Waterline screen that proves the run committed. The gallery mirrors the README "Sample Index" in the sample-app repository — when a sample lands or moves, the README and this gallery move together.
| Pattern | Workflow class | Command | Waterline screen |
|---|---|---|---|
| Smallest deterministic v2 workflow | App\Workflows\Simple\SimpleWorkflow | php artisan app:workflow | Run list → run detail with two activity events and a WorkflowExecutionCompleted event |
| Durable elapsed-time measurement without replay drift | App\Workflows\Elapsed\ElapsedTimeWorkflow | php artisan app:elapsed | Run detail showing two MarkerRecorded events for sideEffect clock reads bracketing a TimerFired event |
| Coordination across Laravel app boundaries | App\Workflows\Microservice\MicroserviceWorkflow | php artisan app:microservice | Run detail showing activity events with task-queue routing across the app and microservice workers |
| Browser automation with captured artifacts | App\Workflows\Playwright\CheckConsoleErrorsWorkflow | php artisan app:playwright | Run detail showing the Playwright activity, the FFmpeg activity, and the cleanup activity in order |
| Webhook-started workflow with a signal wait | App\Workflows\Webhooks\WebhookWorkflow | php artisan app:webhook | Run detail showing WorkflowExecutionStarted from the webhook ingress and a WorkflowExecutionSignaled event for ready |
| AI activity loop with durable retry/validation | App\Workflows\Prism\PrismWorkflow | php artisan app:prism | Run detail showing repeated activity attempts and the ActivityTaskCompleted that satisfies the validator |
| Signal-driven AI agent with saga compensation | App\Workflows\Ai\AiWorkflow | php artisan app:ai | Run detail showing a message-stream MarkerRecorded reference, an Update event, and the compensation activities recorded after a saga failure |
The Waterline screen column names the events you should expect to see in a healthy run; if your local run is missing one, that gap is the fastest way to localize a worker-side or environment problem.
Pattern-page cross-links
The pattern pages on this site link directly into the sample workflow that exercises each surface. Use this table to jump from a pattern page to the matching runnable workflow.
| Pattern page | Sample workflow |
|---|---|
| Sagas | App\Workflows\Ai\AiWorkflow (php artisan app:ai) |
| Signals | App\Workflows\Webhooks\WebhookWorkflow (php artisan app:webhook) |
| Message Streams | App\Workflows\Ai\AiWorkflow (php artisan app:ai) |
| Child Workflows | App\Workflows\Microservice\MicroserviceWorkflow (php artisan app:microservice) |
| Side Effects | App\Workflows\Elapsed\ElapsedTimeWorkflow (php artisan app:elapsed) |
| Webhooks | App\Workflows\Webhooks\WebhookWorkflow (php artisan app:webhook) |
| MCP Workflows | every gallery entry, exposed through config/workflow_mcp.php |
Step 1
Create a codespace from the main branch of this repo.

Step 2
Once the codespace has been created, wait for the codespace to build. This should take between 5 to 10 minutes.
Step 3
Once it is done. You will see the editor and the terminal at the bottom.

Step 4
Run composer install.
composer install
Step 5
Run the init command to setup the app, install extra dependencies and run the migrations.
php artisan app:init
The sample app's php artisan migrate path picks up the workflow and Waterline package migrations directly, so all tables and Waterline saved views are ready after the normal install.
Step 6
Start the queue worker. This will enable the processing of workflows and activities.
php artisan queue:work
When you want to prove the repair loop itself without waiting for a busy queue worker to hit another Looping cycle, run one explicit recovery sweep from a second terminal:
php artisan workflow:v2:repair-pass
That command uses the same scan and backoff policy as the worker loop. It is useful after fixing a local queue/backend issue, while validating a lost-task scenario in the sample app, or when a low-traffic codespace would otherwise take a while to hit another loop pass. Add --run-id=... to limit the sweep to one or more selected runs during an experiment, or --instance-id=... when the whole instance should stay in scope.
Step 7
Create a new terminal window.

Step 8
Start the example workflow inside the new terminal window.
php artisan app:workflow
Step 9
You can view the waterline dashboard at https://[your-codespace-name]-80.preview.app.github.dev/waterline/dashboard.

Waterline is the durable-state view. It answers whether a workflow started, which run is current, which typed history events were committed, which waits are open, and which operator actions are available. Worker-side telemetry is separate: poll latency, task duration, exporter setup, custom application metrics, and worker process errors come from the PHP worker logs or from the SDK metrics endpoint of an external worker.
| Surface | Answers | Sample check |
|---|---|---|
| Waterline and history export | Durable workflow status, history, retries, waits, signals, updates, failures, and operator actions | Open /waterline/dashboard, then export the selected run history |
| Worker logs | PHP queue worker process errors and application log lines | Tail storage/logs/laravel.log while php artisan queue:work is running |
| SDK metrics | External worker/client request counts, poll latency, and task duration | Scrape the SDK worker's Prometheus/OpenMetrics endpoint |
For a Python worker, install the Prometheus extra and expose the worker metrics from that worker process:
pip install 'durable-workflow[prometheus]'
from prometheus_client import start_http_server
from durable_workflow import Client, PrometheusMetrics, Worker
metrics = PrometheusMetrics()
start_http_server(9102)
async with Client("http://localhost:8080", token="secret", metrics=metrics) as client:
worker = Worker(
client,
task_queue="default",
workflows=[GreeterWorkflow],
activities=[greet],
metrics=metrics,
)
await worker.run()
Replace GreeterWorkflow and greet with the workflow and activity handlers
registered by that worker.
Scrape :9102/metrics for durable_workflow_worker_* and
durable_workflow_client_* series. Those metrics explain worker runtime
performance; Waterline remains the source of truth for the committed workflow
history.
Waterline's detail screen includes an "Export History" action for the selected run. When the detail screen is showing the instance's current run, that button uses the instance-scoped current-run export route; historical run detail keeps using the explicit /runs/{runId}/history-export path. You can also export the same replay/debug bundle from the sample app terminal:
php artisan workflow:v2:history-export {workflow-instance-id} --run-id={workflow-run-id} --output=storage/app/workflow-history/example.json --pretty
The export includes a SHA-256 integrity checksum. Set DW_V2_HISTORY_EXPORT_SIGNING_KEY and DW_V2_HISTORY_EXPORT_SIGNING_KEY_ID in the app environment when another system needs to verify the exported bundle with an HMAC signature.
The exported selected_run block includes waits_projection_source, timeline_projection_source, timers_projection_source, and lineage_projection_source. The exported links block also includes projection_source, and the links.parents / links.children sections come from the selected run's typed lineage history first, so child-workflow and continue-as-new relationships remain visible in the bundle even if mutable link rows have drifted during a local experiment. When a lineage row is surviving only through older mutable compatibility data, the bundle now marks that entry with history_authority = mutable_open_fallback and diagnostic_only = true instead of silently rehydrating extra link metadata during export.
AI Workflow Message Streams
Repeated AI or human-input workflows should use the first-class v2 message stream facade as their authoring pattern:
$reply = $this->inbox('ai.assistant')->receiveOne();
$this->outbox('ai.assistant')->sendReference(
targetInstanceId: $this->workflowId(),
payloadReference: $storedReplyReference,
correlationId: $requestId,
);
Keep app-owned payload storage for large request/response bodies, then pass
the stored reference through the stream. Do not teach new sample workflows to
write workflow_messages, MessageStreamCursor, or MessageService calls
directly. See Message Streams for the
stable v2 inbox/outbox contract.
Step 10
Run the workflow and activity tests.
vendor/bin/phpunit
That's it! You can now create and test workflows.
AI Client MCP Server
The sample app also exposes a Laravel MCP server at /mcp/workflows. This is the reference AI-client surface for Durable Workflow v2: it gives agents structured workflow discovery, start, status, output, recent typed history, and failure facts without requiring them to scrape Waterline.
For the detailed MCP endpoint and tool contract, see MCP Workflow Surface. For the broader AI-assisted development contract, including v2 LLM manifests, CLI exit codes, Waterline exports, and SDK references, see AI-assisted development.
The MCP server is registered from routes/ai.php by the Laravel MCP package. The exposed workflow keys live in config/workflow_mcp.php; each entry can include the workflow class plus discovery metadata such as a description, credential requirements, and expected arguments.
Default tools:
| Tool | Purpose |
|---|---|
list_workflows | Lists configured workflow keys, credential requirements, v2 status values, and optionally recent runs. |
start_workflow | Starts a configured v2 workflow and returns workflow_id, run_id, status, business key, and command outcome. |
get_workflow_result | Polls the current or selected run and returns status, output, visibility metadata, and latest failure summary. |
get_workflow_history | Returns a bounded tail of typed v2 history events and latest durable failures for debugging. |
A typical agent loop is:
{"tool": "list_workflows", "arguments": {"show_recent": true, "limit": 5}}
{"tool": "start_workflow", "arguments": {"workflow": "simple", "business_key": "demo-001"}}
{"tool": "get_workflow_result", "arguments": {"workflow_id": "<workflow_id>"}}
{"tool": "get_workflow_history", "arguments": {"run_id": "<run_id>", "limit": 25}}
Use simple or elapsed for no-credential smoke tests. The prism workflow is intentionally exposed as an AI example, but it requires OPENAI_API_KEY before a worker can complete it.