This Laravel 13 application demonstrates Durable Workflow 2.0 through two first-class deployment paths: Service mode with a standalone Server and first-party language workers, or an engine embedded in Laravel. Both paths run in a GitHub Codespace on the supported 2.0 prerelease train. Stable Durable Workflow 2.0 has not been released yet.
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.
Create a Codespace from the main branch of this repository.
Wait while Codespaces pulls the prepared Sample App development image and installs the repository's Composer and npm dependencies. PHP, Node, Composer, Chromium, and operating-system packages are already in the image, so they are not rebuilt for each Codespace.
When setup finishes, choose either deployment path:
| Path | Best fit | Runtime |
|---|---|---|
| Service mode | One approachable run showing the complete first-party language story | PHP workflow + Python activity + Rust activity + standalone Server |
| Embedded Laravel | A Laravel application that owns workflow execution and storage | Laravel app + queue worker |
Service mode is the first-party PHP, Python, and Rust story. After Codespaces
reports that setup is complete, run its featured PolyglotWorkflow sample:
scripts/polyglot.shThis one command resolves the current installable Server, PHP SDK, Python SDK,
and Rust SDK artifacts; builds their worker images; waits for all three workers;
and starts PolyglotWorkflow. The PHP-authored workflow routes an order
calculation to the Python activity queue, sends that calculation to the Rust
receipt activity queue, and combines both results. Its output identifies the
PHP workflow runtime, both activity runtimes, all three task queues, the current
artifact versions, and the completed receipt.
Docker and the complete PHP/Python/Rust toolchain are included in the prepared
Codespaces image. The command does not require a package-install step or any
tool outside the repository and its Compose stack. Repeat it for another run;
the isolated sample-app-polyglot-demo project remains available between runs.
The exhaustive directional codec, replay, signal, query, and Waterline checks
remain in the polyglot matrix guide.
For framework integration work, an optional narrower variation uses the standalone Server with Laravel's SDK bridge plus PHP and Python activities:
scripts/service-mode.shIt remains documented as an advanced Laravel integration variation, not a separate deployment path.
Codespaces setup has already created the environment, generated the application key, migrated the database, and verified MySQL, Redis, and Playwright. Start Laravel's web, queue, log, and asset processes:
composer run devIn a second terminal, start the example workflow:
php artisan app:workflowOpen Waterline at the forwarded port 18080 URL under /waterline. The workflow
result also appears in the terminal. Run the workflow and activity tests with:
php artisan testThe embedded stack uses the Codespace's sample-app Compose state. Workflow
instances receive generated identities, so the command is safe to run again.
Check the two observability surfaces separately:
| Surface | Use it for | Where to look |
|---|---|---|
| Waterline and the workflow database | Durable workflow truth: run status, typed history, signals, updates, timers, retries, failures, and operator actions. | The run URL printed by application journeys, or /waterline when the selected stack includes it |
| Worker logs and SDK metrics | Runtime behavior: poll latency, task duration, exporter wiring, custom application metrics, and worker-side errors before they become durable failures. | Laravel logs for PHP workers; SDK metrics endpoints for external workers |
Waterline proves that the durable run exists and shows what the engine or standalone Server committed. Worker metrics remain a separate runtime surface. Minimal Python worker Prometheus wiring uses the moving 2.0 prerelease constraint rather than a copied release-candidate number:
python -m pip install --pre 'durable-workflow[prometheus]~=2.0'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. Use Waterline for the matching workflow history and status.
The default devcontainer Compose file pulls
ghcr.io/durable-workflow/sample-app-devcontainer:main. The same image is
available from Docker Hub as
durableworkflow/sample-app-devcontainer:main; select it without changing the
Compose file by setting SAMPLE_APP_DEVCONTAINER_IMAGE before opening the
devcontainer. Both channels support Linux AMD64 and ARM64.
main is the moving channel built from protected main pushes and the weekly
refresh. The Chromium revision follows the exact Playwright version in
package-lock.json, which is covered by the weekly dependency update path.
Every publication is retained under an immutable
sha-<source-revision>-run-<workflow-run>-<attempt> tag in both registries.
Published indexes include OCI source/revision labels, BuildKit provenance, and
an SPDX SBOM. The moving tags advance only after both registry copies and both
architectures pass the unauthenticated Compose qualification.
The prepared image also runs the OpenSSH server expected by Codespaces tooling, so remote shell and creation-log access do not require a per-Codespace feature install.
There is deliberately no local Dockerfile fallback in the Codespaces Compose topology. A pull or qualification failure stops setup instead of reconstructing the old operating-system environment. To qualify a published image manually and write phase timings to a JSON file, run:
scripts/ci/qualify-devcontainer-image.sh \
ghcr.io/durable-workflow/sample-app-devcontainer:main \
linux/amd64 \
devcontainer-qualification-timing.jsonThe qualification pulls the selected image, starts the same MySQL/Redis and
Laravel/microservice topology with --no-build, installs only repository
dependencies, launches Chromium as the non-root laravel user, verifies the
SSH endpoint, checks the application health endpoint and mounted-checkout
editability, and records fresh and warm startup timings.
Prefer a local workstation over Codespaces? The repository ships a
docker-compose.yml that builds and runs the app, worker, MySQL, and Redis on
any host with Docker Engine and Docker Compose v2 installed.
# 1. Clone and enter the repo
git clone https://github.com/durable-workflow/sample-app.git
cd sample-app
# 2. (Optional) expose the app on a non-default port
export APP_PORT=18080
# 3. Build and start the stack. --wait blocks until health checks pass.
docker compose up -d --build --wait app worker
# 4. Run migrations against the shared sample database.
docker compose exec -T app php artisan migrate:fresh --force
# 5. Run the simplest deterministic sample end-to-end.
docker compose exec -T app php artisan app:workflowOnce the stack is up, Waterline is at http://localhost:${APP_PORT:-8000}/waterline/dashboard
and the MCP server is at http://localhost:${APP_PORT:-8000}/mcp/workflows.
For a release-style proof from a clean checkout, use the combined entry point instead of the manual build, migration, and sample commands above. It builds the resolved artifact tuple once, runs deterministic smoke, and continues through the provider-free conformance matrix on the same healthy stack and schema. AI surfaces are recorded as intentional skips, so this command needs no provider credential:
scripts/compose-smoke-conformance.shThe standalone full-conformance entry point remains self-contained for callers that do not need the deterministic preflight. AI-backed surfaces are disabled by default, so this form cannot consume a provider credential discovered in the shell or an ancestor dotenv file:
scripts/compose-conformance.shThe harness emits a JSON document with the sample-app commit, artifact versions,
timestamp, per-surface outcome, focused findings, setup measurements, and any
skipped surfaces. Setup measurements include whether the run started with a
clean or warm image cache, setup duration, peak Docker disk growth, build
invocation count, and whether a prepared stack was reused. Run the combined
entry point once without its app image and again with the resulting cache to
capture comparable clean-cache and warm-cache measurements. It runs the documented
artisan samples, browser checks for the app and Waterline, the MCP workflow API,
an API documentation check that compares the README's documented MCP tools and
workflow keys with the live endpoint, a Waterline/manual observation check using
workflow:v2:history-export, local sandbox lifecycle variants, sandbox recovery
injection, and, in explicit provider mode, the Prism/AI samples. The Prism check
uses OPENAI_API_KEY for the live model-backed AI surface. The travel-agent success and failure-injection
checks reuse one deterministic booking plan so the run proves signals, durable
assistant messages, booking activities, and compensation without spending extra
model calls on each failure variant.
The polyglot harness builds the exact released Rust SDK from crates.io and
executes Rust-authored workflows and activities across the PHP, Python, and
Rust runtime matrix. Its report distinguishes registered Rust execution from
the release-cohort version pin.
SAMPLE_APP_CONFORMANCE_SKIP_AI=1 is the safe release and automation mode. It
passes --skip-ai to app:conformance, keeps OPENAI_API_KEY out of Compose
exec arguments, and records every AI-backed surface as explicitly skipped while
the deterministic and scripted agent-operability surfaces still run. Intentional
AI skips are allowed automatically in this mode; combining them with --strict
is rejected as a contradictory coverage request.
Provider-backed conformance requires an explicit opt-in, even when a credential is already present. Its release proof is:
export OPENAI_API_KEY=your-provider-key
SAMPLE_APP_CONFORMANCE_SKIP_AI=0 \
scripts/compose-smoke-conformance.sh --strictSet SAMPLE_APP_CONFORMANCE_ENV_FILE only on that opt-in path when the key lives
in a dotenv file outside the repository; the wrapper then checks local
workspace-level dotenv files without printing credential values. Provider mode
requires strict coverage by default, and --strict makes that requirement
explicit. Without AI credentials, the run stays non-passing and names the live
Prism surface as uncovered. Set
DURABLE_SERVER_IMAGE, DURABLE_WORKFLOW_CLI_VERSION,
DURABLE_WORKFLOW_PYTHON_SDK_VERSION, DURABLE_WORKFLOW_RUST_SDK_VERSION,
DURABLE_WORKFLOW_PHP_SDK_VERSION, DURABLE_WORKFLOW_WORKFLOW_VERSION, and
DURABLE_WORKFLOW_WATERLINE_VERSION to override the published artifact set.
The PHP SDK variable selects the framework-neutral durable-workflow/sdk
package used by polyglot/; the Workflow variable selects the separate
durable-workflow/workflow engine used by this Laravel application.
By default, the wrapper calls
scripts/resolve-current-artifacts.sh, which resolves one 2.0 prerelease
channel from the public docs release-audit manifest. Beta tuples remain
synchronized, while release-candidate tuples may contain component-specific
increments as long as every component stays in the rc channel.
| Prerelease channel | Component-version policy |
|---|---|
beta |
synchronized |
rc |
component-specific |
mixed |
rejected |
The resolver emits the accepted tuple as shell assignments and preserves
explicit overrides. The wrapper rebuilds the app and worker containers with the
resolved PHP SDK, Workflow, and Waterline pins before running the harness, so
the recorded versions come from installed packages rather than the committed
fallback lock. The polyglot Rust image likewise applies the resolved SDK version
to its build-local manifest, leaving the committed Cargo manifest and lock as
the pinned fallback. The standalone PHP stack independently installs and
executes the resolved PHP SDK pin. Set
DURABLE_WORKFLOW_ARTIFACT_SOURCE=pinned for a reproducible run against the
committed sample-app fallback tuple instead. Set
DURABLE_WORKFLOW_ARTIFACT_TUPLE_FILE=/path/to/tuple.json when a local run
should use a previously captured public tuple manifest.
The wrapper passes the host checkout SHA into the app container as
SAMPLE_APP_COMMIT; set that variable explicitly when running from a source
archive or another environment without Git metadata. The same value is forwarded
as a Docker build and runtime variable so source-free containers can report the
sample-app revision without reading a local .git checkout.
The wrapper also copies the JSON metadata back to
storage/app/sample-app-conformance-metadata.json; set
SAMPLE_APP_CONFORMANCE_METADATA_PATH to choose a different host-side path.
Pass that file as DW_AGENT_OPERABILITY_SAMPLE_APP_METADATA_PATH when validating
the agent-operability executable-loop contract against the current artifact
tuple.
The app service has the browser-safe sample-app network alias, and the wrapper
uses http://sample-app:8000 inside the Compose network so browser activities
running in the worker container can reach the app without an HTTPS upgrade. Set
SAMPLE_APP_CONFORMANCE_URL when running against a different network address.
The wrapper derives one coverage policy from the AI mode: provider-free mode
allows its intentional AI skips, while provider mode is strict by default. Set
SAMPLE_APP_CONFORMANCE_ALLOW_SKIPS=1 only for exploratory provider-mode runs
that should return zero while naming missing provider-backed evidence.
scripts/compose-smoke.sh starts with the bounded deterministic preflight: it
runs the deterministic samples and exits after printing the blocked step,
container status, and recent app/worker logs on failure. By default, a passing
preflight continues into the broader public sample-app conformance surface so a
release/conformance caller does not accidentally record deterministic smoke as
full coverage. The handoff records the prepared app and worker containers; the
full wrapper reuses them only when their health, artifact tuple, credentials,
installed packages, and migrated schema still match. Otherwise it falls back to
its self-contained rebuild and schema reset. Set SAMPLE_APP_SMOKE_ONLY=1 when
a caller intentionally wants only the deterministic path. Set
SAMPLE_APP_CONFORMANCE_AFTER_SMOKE=0 to disable the chained full surface for
exploratory local runs, or run scripts/compose-conformance.sh --strict
directly with SAMPLE_APP_CONFORMANCE_SKIP_AI=0 when a strict provider run does
not need the deterministic preflight.
Tear the stack down with docker compose down -v --remove-orphans when
finished. The deterministic Docker path is exercised on every push through the
smoke GitHub Actions workflow, and the full harness is available for release
and conformance checks that have the required credentials.
Use this index when you want a specific Durable Workflow pattern instead of another happy-path snippet.
| Goal | Workflow | Command | MCP key |
|---|---|---|---|
| Learn the smallest v2 workflow/activity shape | App\Workflows\Simple\SimpleWorkflow |
php artisan app:workflow |
simple |
| Measure durable elapsed time without replay drift | App\Workflows\Elapsed\ElapsedTimeWorkflow |
php artisan app:elapsed |
elapsed |
| Coordinate work across Laravel app boundaries | App\Workflows\Microservice\MicroserviceWorkflow |
php artisan app:microservice |
microservice |
| Run browser automation and collect generated artifacts | App\Workflows\Playwright\CheckConsoleErrorsWorkflow |
php artisan app:playwright https://example.com |
playwright |
| Start from an external webhook and wait for a signal | App\Workflows\Webhooks\WebhookWorkflow |
php artisan app:webhook |
webhook |
| Wrap an AI activity loop in durable retry/validation | App\Workflows\Prism\PrismWorkflow |
php artisan app:prism |
prism |
| Build a signal-driven AI agent with compensation | App\Workflows\Ai\AiWorkflow |
php artisan app:ai |
ai |
| Orchestrate an ephemeral agent sandbox with durable lifecycle | DurableWorkflow\AI\Workflows\SandboxAgentWorkflow |
php artisan app:sandbox |
sandbox |
| Run one PHP workflow that combines Python and Rust activity results | App\Workflows\Polyglot\PolyglotWorkflow |
scripts/polyglot.sh |
polyglot |
| Exercise machine-readable failure diagnosis and repair refusal | App\Workflows\Diagnostics\DiagnosticFailureWorkflow |
/mcp/workflows start_workflow with workflow=diagnostic_failure |
diagnostic_failure |
Porting a workflow from the v1 generator API to the v2 Fiber API is mechanical. The v1 sources live on the Laravel-12 branch; use it as a side-by-side reference while you migrate.
Workflow shape:
- Extend
Workflow\V2\Workflowinstead ofWorkflow\Workflow. - Import helpers from the
Workflow\V2\namespace:use function Workflow\V2\{activity, sideEffect, await, timer};. - Replace
yield activity(...)with a straight-lineactivity(...)call — the Fiber runtime suspends transparently. - Rename the entry method from
execute(...)tohandle(...)and add return types.
Activities:
- Extend
Workflow\V2\Activityand definehandle(...)with typed parameters and return type. Activities are invoked by class name from workflow code, for exampleactivity(SimpleActivity::class).
Signals, updates, webhooks:
- Signals shifted from push to pull. Import the class-level contract attribute with
use Workflow\V2\Attributes\Signal;, declare it as#[Signal('name', [...])], and block onawait('name')insidehandle()to receive each delivery;await('name', $timeout)returnsnullon timeout for chat-style loops. #[UpdateMethod]and#[QueryMethod]carry over verbatim.- From the outside, use explicit names:
$workflow->signal('name', $payload)and$workflow->update('name', ...). - Webhook routing now takes an explicit alias map:
Workflow\V2\Webhooks::routes(['webhook-workflow' => WebhookWorkflow::class]);.
Compensation closures:
addCompensation(callable)andcompensate()on the v2Workflowbase class are unchanged. Dropyield frominside the closures:addCompensation(fn () => activity(CancelHotelActivity::class, $hotel));.
Stub usage:
- Use
Workflow\V2\WorkflowStub. Themake(),load(),start(),running(),output(),signal(), andupdate()methods carry over; poll with$stub->refresh()->running()and a smallusleep(100_000)between checks instead of a tight loop.
The App\Workflows\Simple\SimpleWorkflow, App\Workflows\Webhooks\WebhookWorkflow, and App\Workflows\Ai\AiWorkflow samples in this repo are the canonical references for the basic shape, webhook entry, and signal/update agent patterns respectively.
Use message streams when a workflow needs to publish or consume repeated messages without writing Durable Workflow storage rows directly. The v2 authoring API is exposed through Workflow::inbox(), Workflow::outbox(), and Workflow::messages(); those facades own workflow_messages rows and stream cursor advancement for the workflow run.
App\Workflows\Ai\AiWorkflow is the reference sample. It stores large assistant payloads in the app-owned ai_workflow_messages table, then publishes only a durable reference on the ai.assistant stream:
$this->outbox(self::ASSISTANT_STREAM)
->sendReference(
$this->workflowId(),
$reference,
correlationId: $reference,
idempotencyKey: $reference,
metadata: ['role' => 'assistant'],
);The receive update consumes the next assistant reply through the matching inbox stream:
$streamMessage = $this->inbox(self::ASSISTANT_STREAM)
->receiveOne();receiveOne() consumes the message and advances the durable stream cursor, so repeated receives deliver new replies instead of replaying old ones. Keep app tables as payload/reference stores; let Durable Workflow own workflow_messages and stream cursor advancement through the facade.
Long-running coding agents need an ephemeral workspace, but lifecycle and
recovery infrastructure should not be copied into each application. This app
consumes durable-workflow/ai; the package owns the versioned provider contract,
activities, DurableWorkflow\AI\Workflows\SandboxAgentWorkflow, E2B and local
adapters, stable operation IDs, post-snapshot reconstruction, leases, and
cleanup. The Sample App retains only its command, configuration example, and
end-to-end demonstration.
config/durable-workflow-ai.php selects the provider. The default local
subprocess provider is development/test-only, runs with the worker's privileges,
and is not a security isolation boundary. The E2B adapter uses the documented
HTTP API. This sample does not expose E2B suspend/resume because paused
sandboxes have no provider TTL; it must not be enabled without an independent
durable cleanup deadline. Both built-in providers explicitly declare
at-least-once tool effects; a lost acknowledgement can repeat a mutating call.
Run the sample with:
php artisan app:sandbox # local subprocess provider
php artisan app:sandbox --snapshot-every=2 # snapshot every 2 tool calls
php artisan app:sandbox --snapshot-every=2 --inject-loss-after=2 # force local restore
DURABLE_AI_SANDBOX_DRIVER=e2b E2B_API_KEY=… php artisan app:sandboxSee docs/sandbox-orchestration.md for the integration walkthrough and links to the package's delivery contract and provider-author guide.
The repository ships a runnable polyglot demonstration in
polyglot/. It brings up the standalone Durable Workflow
server with framework-neutral PHP workers from the published
durable-workflow/sdk package, Python workers, and crates.io-installed Rust
workers side by side. The root Laravel example remains a separate embedded
mode backed by durable-workflow/workflow. Nine workflow/activity runtime
cells run end to end:
- a Python-authored workflow on its own Python image, and
- a PHP-authored workflow in
polyglot/php_worker/worker.phpthat schedulespolyglot.php-to-python.*activities handled by the Python worker on a shared task queue, and - a Python-authored workflow that schedules
polyglot.python-to-php.*activities handled by a distinct PHP activity worker. - Rust-authored workflows and activities run same-language, Rust-to-PHP, Rust-to-Python, PHP-to-Rust, and Python-to-Rust cells.
The cross-language scenarios are wire-level tests: the workflow
runtime and activity runtime register separately, and each scheduled
activity crosses the language boundary on the wire — not just inside
one process. The smoke runs in CI on every pull request via
.github/workflows/polyglot-validation.yml, so a regression in either
direction is caught before release rather than in the field.
The codec round-trip rules — which payload values cross the language
boundary cleanly and which need explicit adapters — are documented in
the workflow package at
docs/architecture/polyglot-codec-roundtrip.md.
Operators of polyglot fleets should treat the "requires an explicit
adapter" set as a workflow-author contract: the SDKs fail closed at the
boundary rather than guess at a serialisation.
Durable Workflow v2 replays workflow code to rebuild local state from committed history. Keep workflow methods deterministic: call activities for side effects, use sideEffect() for values such as timestamps or random IDs, and wait for outside input through signals, updates, timers, or message streams.
Do this when a workflow needs the current time:
use function Workflow\V2\sideEffect;
$startedAt = sideEffect(fn () => now()->getTimestamp());Don't do this inside workflow code:
$startedAt = now();The direct now() call looks harmless, but replay can run the method again later and produce a different value than the one that originally drove branching, timeouts, or output. Prefer scalar values inside sideEffect() callbacks — integer timestamps, ISO-8601 strings, UUIDs — so the recorded value survives any configured payload codec on replay; returning a Carbon instance can decode as a plain string under non-JSON codecs such as Avro. The ElapsedTimeWorkflow sample keeps clock reads behind sideEffect() as integer timestamps, and the SimpleWorkflow, PrismWorkflow, and AiWorkflow samples keep external work inside activities for the same reason.
In addition to the basic example workflow, you can try these other workflows included in this sample app:
-
php artisan app:elapsed– Demonstrates how to correctly track start and end times to measure execution duration. -
php artisan app:microservice– A fully working example of a workflow that spans multiple Laravel applications using a shared database and queue. -
php artisan app:playwright– Runs a Playwright automation againsthttps://example.com, captures a WebM video, encodes it to MP4 using FFmpeg, and then cleans up the WebM file. Pass a URL to check another page, for examplephp artisan app:playwright http://localhost:8000/waterline/dashboard. -
php artisan app:webhook– Showcases how to use the built-in webhook system for triggering workflows externally. -
php artisan app:prism- Uses Prism to build a durable AI agent loop. It asks an LLM to generate user profiles and hobbies, validates the result, and retries until the data meets business rules. -
php artisan app:ai- NEW! Uses Laravel AI SDK to build a durable travel agent. The agent asks questions and books hotels, flights, and rental cars. If a booking error occurs, the workflow ensures prior bookings are canceled; an inactivity timeout closes the conversation without rolling back successful interactive bookings. For repeatable checks, pass one or more--message="..."options and optionally--inactivity-timeout=5; use--inject-failure=hotel,--inject-failure=flight, or--inject-failure=carto exercise compensation.--booking-plan-json='{"text":"...","bookings":[...]}'lets deterministic scripted checks run a single planned turn while still exercising the workflow, booking activities, and compensation. -
php artisan app:sandbox- Package integration demo fordurable-workflow/ai. The command dispatches a short tool sequence through the reusable sandbox workflow. Use--snapshot-every=2 --inject-loss-after=2to exercise local recovery, or setDURABLE_AI_SANDBOX_DRIVER=e2bplusE2B_API_KEYto use E2B Cloud. The local subprocess provider is development/test-only and is not a security isolation boundary; E2B suspend/resume is unavailable until paused resources have an independent durable cleanup deadline.
Try them out to see workflows in action across different use cases!
This sample app includes an MCP (Model Context Protocol) server that allows AI clients (ChatGPT, Claude, Cursor, etc.) to start and monitor Durable Workflow v2 workflows. Treat it as the agent-operable companion to Waterline: humans can inspect /waterline/dashboard, while AI clients receive structured workflow IDs, run IDs, statuses, recent typed history, and failure summaries.
The MCP server is named Durable Workflow.
It is not a separate daemon in this repo. The server is exposed by the Laravel application itself, so once the app is running, the MCP route is live as part of the normal HTTP server.
The MCP server is available at: /mcp/workflows
To make the MCP server available locally:
- Run
php artisan app:init - Start the queue worker with
php artisan queue:work redis --queue=default,activity - Start the Laravel app with
php artisan serve - Connect your MCP client to
http://localhost:8000/mcp/workflows
If you prefer Docker, run docker compose up --build, then run docker compose exec app php artisan migrate --force once the containers are healthy. After migrations complete, connect to http://localhost:8000/mcp/workflows.
| Tool | Description |
|---|---|
list_workflows |
Discover configured workflow keys, credential requirements, status values, and recent v2 runs |
start_workflow |
Start a configured v2 workflow asynchronously and get a workflow instance ID plus run ID |
get_workflow_result |
Check workflow status, output, visibility metadata, and latest failure summary |
get_workflow_history |
Inspect a bounded slice of typed v2 history events and latest durable failures |
diagnose_workflow |
Summarize health facts, root-cause classification, remediation, latest failure evidence, and safe next actions for stuck or failed runs |
repair_workflow |
Request the built-in v2 repair command and receive a structured accepted, refused, or not-needed mutation result |
Available workflows are defined in config/workflow_mcp.php. By default, every workflow in the sample index is exposed:
simple→App\Workflows\Simple\SimpleWorkflowelapsed→App\Workflows\Elapsed\ElapsedTimeWorkflowmicroservice→App\Workflows\Microservice\MicroserviceWorkflowplaywright→App\Workflows\Playwright\CheckConsoleErrorsWorkflow(requires local Playwright/Node/FFmpeg setup)webhook→App\Workflows\Webhooks\WebhookWorkflow(waits for thereadysignal)prism→App\Workflows\Prism\PrismWorkflow(requiresOPENAI_API_KEY)ai→App\Workflows\Ai\AiWorkflow(requiresOPENAI_API_KEY, then acceptssendsignals andreceiveupdates)sandbox→DurableWorkflow\AI\Workflows\SandboxAgentWorkflow(package-owned lifecycle and recovery; defaults to the development-only local subprocess provider, setDURABLE_AI_SANDBOX_DRIVER=e2bplusE2B_API_KEYfor E2B Cloud)polyglot→App\Workflows\Polyglot\PolyglotWorkflow(runscripts/polyglot.shto start the standalone PHP workflow worker plus distinct Python and Rust activity workers against the current artifact tuple)diagnostic_failure→App\Workflows\Diagnostics\DiagnosticFailureWorkflow(no credentials; intentionally records a durable activity failure so MCP clients can provediagnose_workflowandrepair_workflowbehavior)
To add more workflows, update the config file:
'workflows' => [
'simple' => [
'class' => App\Workflows\Simple\SimpleWorkflow::class,
'description' => 'Small deterministic workflow.',
'pattern' => 'deterministic activity chain',
'command' => 'php artisan app:workflow',
'requires' => [],
'arguments' => [],
],
'my_workflow' => [
'class' => App\Workflows\MyWorkflow::class,
'description' => 'What an agent should know before starting it.',
'requires' => ['EXTERNAL_API_KEY'],
'arguments' => [
['name' => 'customer_id', 'type' => 'string'],
],
],
],Class-string mappings are still accepted for small local experiments, but the array form gives agents safer discovery metadata.
An AI client would typically:
- Call
list_workflowsto see available workflows - Call
start_workflowwith{"workflow": "simple", "business_key": "demo-001"} - Receive
workflow_idandrun_idin the response - Poll
get_workflow_resultwith theworkflow_iduntil status iscompleted - Read the
outputfield for the workflow result - If status is
failedorwaitinglonger than expected, calldiagnose_workflow - Read
root_cause.category,remediation.classification, andremediation.automatic_repair.allowed - Call
repair_workflowonly when remediation marks repair as allowed, then pollget_workflow_resultand inspectget_workflow_historywith therun_id
Use the structured templates under Issues so reproducers and sample requests land with the metadata maintainers need:
- Bug reproducer. A reproducer runs in this app: it names the workflow class, the artisan command, the Durable Workflow package version, and the observed durable failure. Reproducers that follow the template land as new workflows under
app/Workflows/Bug/<issue>/and stay covered by CI after the bug is fixed. - Sample request. A sample request names the Durable Workflow pattern that is not yet covered, the public docs page that defines it, and the minimum package version it needs. Requests close when a workflow under
app/Workflows/exercises the pattern end to end and is wired into the artisan command list andconfig/workflow_mcp.php.
Bugs in the workflow engine itself or the standalone Durable Workflow server belong on the workflow and server repos respectively; the issue chooser links those out.
Have a Durable Workflow pattern you want to share? Read CONTRIBUTING.md for the full contract — workflow class layout, artisan command name, MCP entry, test, README index row, and the docs-site gallery and pattern-page cross-link that ship in the same change. The Contribute a Sample page on the docs site is the canonical version of the same guide.
Maintainers tagging an upstream release should read
docs/release-notes-feature-contract.md
first; it names the bar a sample must meet to be cited in upstream
release notes and the checklist that runs before a release tag lands.
This is a public repository. Do not add private tracker names, workspace-only absolute paths, or loop/lane metadata to files or new commit metadata. Run scripts/check-public-boundary.sh before publishing changes; CI runs the same scan on pushes and pull requests.
