Skip to main content
Version: 2.0 prerelease

CLI

The Durable Workflow CLI (dw) is a shell interface to the standalone server. It lets operators start, list, signal, query, update, repair, cancel, terminate, and archive workflows, manage schedules, inspect task queues, and check server health — all from the command line.

The same CLI works against any Durable Workflow server, regardless of which language your workflows are written in. For side-by-side examples of the same operation through dw and the Python SDK, see CLI and Python parity.

Install

curl -fsSL https://durable-workflow.com/install.sh | sh

Verify

dw --version

Pinned install for CI and quickstarts

CI jobs and quickstart instructions should install a specific release rather than latest, so the same command produces the same dw binary every time and upgrades only when you change the pin.

Both installer scripts download SHA256SUMS from the release and verify the asset checksum before replacing dw, so a tampered mirror fails the install.

Linux and macOS (shell installer)
curl -fsSL https://durable-workflow.com/install.sh | VERSION=0.1.75 sh
dw --version

VERSION accepts any published release tag. Leave it unset to install the latest release. Additional environment variables:

  • DURABLE_WORKFLOW_INSTALL_DIR — install location (default ~/.local/bin).
  • DURABLE_WORKFLOW_BIN_NAME — installed executable name (default dw).

A GitHub Actions example:

- name: Install Durable Workflow CLI
run: |
curl -fsSL https://durable-workflow.com/install.sh | VERSION=0.1.75 sh
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Verify
run: dw --version
Windows (PowerShell installer)
$env:VERSION = "0.1.75"
irm https://durable-workflow.com/install.ps1 | iex
dw --version

The installer writes dw.exe to %USERPROFILE%\.durable-workflow\bin and adds that directory to the user PATH.

PHAR (portable, requires PHP 8.2+)
VERSION=0.1.75
curl -fsSL -o dw.phar \
"https://github.com/durable-workflow/cli/releases/download/${VERSION}/dw.phar"
curl -fsSL -o SHA256SUMS \
"https://github.com/durable-workflow/cli/releases/download/${VERSION}/SHA256SUMS"
sha256sum --check --ignore-missing SHA256SUMS
chmod +x dw.phar
./dw.phar --version

The PHAR works wherever PHP 8.2 or newer is already available and is the recommended artifact for shared CI runners that already ship PHP.

Update An Installed Binary

For standalone release installs (the shell and PowerShell installers above), dw upgrade replaces the running binary with the latest published release after verifying the asset against the release's SHA256SUMS.

dw upgrade                 # upgrade to the latest release
dw upgrade --tag=0.1.75 # pin to a specific release tag
dw upgrade --dry-run # resolve the target release without downloading

dw upgrade refuses to rewrite Composer vendor, Homebrew cellar, and PHAR installs because those paths are owned by another tool. Reinstall a pinned public release with the installer, update the owning package manager, or use brew upgrade durable-workflow/tap/dw for tap-managed Homebrew installs. See the CLI reference for the full stable option and status-field contract.

Configure

Point the CLI at your server:

export DURABLE_WORKFLOW_SERVER_URL=http://localhost:8080
export DURABLE_WORKFLOW_AUTH_TOKEN=your-token
export DURABLE_WORKFLOW_NAMESPACE=default

Or pass them per-command:

dw --server=http://localhost:8080 --token=your-token workflow:list

Five-Minute Operator Quickstart

This path is for checking the CLI against a real standalone server without writing application code. It starts the published local server stack, installs a pinned dw, creates a reusable profile, starts one workflow, and watches the run reach the worker queue.

export DW_SERVER_IMAGE=durableworkflow/server:0.2.261
export DW_AUTH_TOKEN=dev-token

docker volume create durable-workflow-cli-quickstart

docker run --rm \
-v durable-workflow-cli-quickstart:/app/database \
-e DW_AUTH_DRIVER=token \
-e DW_AUTH_TOKEN="$DW_AUTH_TOKEN" \
"$DW_SERVER_IMAGE" server-bootstrap

docker rm -f durable-workflow-server >/dev/null 2>&1 || true
docker run -d --name durable-workflow-server \
-p 8080:8080 \
-v durable-workflow-cli-quickstart:/app/database \
-e DW_AUTH_DRIVER=token \
-e DW_AUTH_TOKEN="$DW_AUTH_TOKEN" \
"$DW_SERVER_IMAGE"

until curl -sf http://localhost:8080/api/ready >/dev/null; do sleep 1; done

Install the CLI in another terminal:

curl -fsSL https://durable-workflow.com/install.sh | VERSION=0.1.75 sh
export PATH="$HOME/.local/bin:$PATH"
dw --version

Save the server connection once:

dw env:set local \
--server=http://localhost:8080 \
--token=dev-token \
--namespace=default \
--make-default

dw doctor
dw server:health

Start a workflow and inspect the run:

dw workflow:start \
--type=quickstart.order \
--workflow-id=quickstart-order-001 \
--task-queue=quickstart \
--input='{"order_id":"order-001","total":42.50}' \
--json

dw workflow:describe quickstart-order-001 --output=json
dw workflow:history quickstart-order-001 <run-id-from-start-or-describe>
dw task-queue:describe quickstart

The run is now durable server state. Until a worker for quickstart.order polls the quickstart queue, dw task-queue:describe quickstart is the fastest way to see why the run is waiting. When a worker is attached, use dw watch workflow quickstart-order-001 to follow the run to a terminal state. For an end-to-end run that attaches a published Python SDK worker and reaches status=completed, follow the 2.0 prerelease quickstart.

Commands

Server

dw server:health          # Check server health
dw server:info # Show server version, role topology, protocols, and worker fleet

dw server:info mirrors GET /api/cluster/info. In addition to build, protocol, and worker-fleet facts, it prints the server role-topology manifest: supported shapes, the current shape/process class/roles, matching-role wake and partition details, current write boundaries, scaling boundaries, and failure domains. Use --output=json when automation needs the raw topology.* fields, or read Server Role Topology for the field-by-field contract behind that output.

Workflows

dw workflow:list                                          # List workflows
dw workflow:start --type=MyWorkflow --input='["arg1"]' # Start a workflow
dw workflow:start --type=MyWorkflow --input-file=input.json
dw workflow:describe <workflow-id> # Describe a workflow
dw workflow:signal <workflow-id> <signal-name> --input='["ok"]'
dw workflow:query <workflow-id> <query-name> # Run a query
dw workflow:cancel <workflow-id> # Request cancellation
dw workflow:terminate <workflow-id> # Force terminate
dw workflow:history <workflow-id> <run-id> # Show run history

Every command that accepts caller payloads uses the same input shape: --input for inline values, --input-file for a file path or - for stdin, and --input-encoding=json|raw|base64. JSON is the default; raw and base64 inputs are passed as one positional workflow argument.

Bridge Adapters

dw bridge:webhook stripe \
--action=start_workflow \
--idempotency-key=stripe-event-1001 \
--target='{"workflow_type":"orders.fulfillment","task_queue":"external-workflows"}' \
--input='{"order_id":"order-1001"}'

dw bridge:webhook pagerduty \
--action=signal_workflow \
--idempotency-key=pd-event-3003 \
--target='{"workflow_id":"wf-remediation-42","signal_name":"incident_escalated"}' \
--input='{"severity":"critical"}' \
--json

Bridge adapters are bounded ingress tools for integration events. They return named bridge outcomes for accepted, duplicate, and rejected deliveries; they do not become workflow runtimes.

Schedules

dw schedule:list                          # List schedules
dw schedule:create --workflow-type=MyWorkflow \
--cron='0 * * * *' # Create hourly schedule
dw schedule:update <schedule-id> --input-file=input.json
dw schedule:describe <schedule-id> # Describe a schedule
dw schedule:pause <schedule-id> # Pause a schedule
dw schedule:resume <schedule-id> # Resume a schedule
dw schedule:trigger <schedule-id> # Trigger immediately
dw schedule:delete <schedule-id> # Delete a schedule

Activities

dw activity:complete <task-id> <attempt-id> --input='{"ok":true}'
dw activity:complete <task-id> <attempt-id> --input-file=result.json
dw activity:fail <task-id> <attempt-id> --message='upstream failed'

Workers and Task Queues

dw worker:list                    # List registered workers
dw worker:describe <worker-id> # Describe a worker
dw task-queue:list # List active task queues
dw task-queue:describe <queue> # Describe a task queue

dw task-queue:list is the compact fleet view. It shows admission status columns for workflow, activity, and query tasks. dw task-queue:describe expands the same server payload with pollers, current leases, queue, namespace, and downstream budget-group dispatch capacity, remaining capacity, budget source, and query-task pending capacity.

For scripts, dw task-queue:describe --json exposes queue-local backlog, lease, poller, and admission state. Use it to distinguish missing workers from admission throttling or a single queue that is building backlog:

dw task-queue:describe orders --json | jq '.stats | {
approximate_backlog_count,
approximate_backlog_age_seconds,
workflow_tasks,
activity_tasks
}'

Fleet-level durable inflow versus dispatch rates do not come from the task-queue JSON contract. Read them from dw system:operator-metrics --json instead:

dw system:operator-metrics --json | jq '.operator_metrics.backlog | {
tasks_added_last_minute,
tasks_dispatched_last_minute
}'

See Task Queue Admission for the operator tuning contract behind those fields.

To inspect the matching-role routing contract on the node you queried, read the same payload's matching_role block:

dw system:operator-metrics --json | jq '.operator_metrics.matching_role | {
queue_wake_enabled,
shape,
task_dispatch_mode,
partition_primitives,
backpressure_model
}'

partition_primitives freezes the routing axes (connection, queue, compatibility, namespace) and backpressure_model tells you whether the engine is relying on lease occupancy or some other admission boundary. Current v2 reports lease_ownership.

Namespaces

dw namespace:list                             # List namespaces
dw namespace:create --name=production # Create a namespace
dw namespace:describe <namespace> # Describe a namespace

Search Attributes

dw search-attribute:list                                  # List search attributes
dw search-attribute:create --name=env --type=keyword # Register an attribute

Exit Codes

The CLI uses a stable exit-code policy so scripts and CI pipelines can react to specific failure modes without parsing stderr. Values follow Symfony Console's canonical 0/1/2 for success / failure / usage, and extend from there.

CodeNameMeaning
0SUCCESSOperation completed successfully.
1FAILUREGeneric failure — command ran but did not succeed.
2INVALIDInvalid usage — bad arguments, unknown options, or local validation. Also returned for HTTP 4xx responses that are not covered below (e.g. 400, 422).
3NETWORKCould not reach the server (connection refused, DNS failure, TLS handshake failure, transport error).
4AUTHAuthentication or authorization failure. Returned for HTTP 401 and 403.
5NOT_FOUNDResource not found. Returned for HTTP 404.
6SERVERServer error. Returned for HTTP 5xx.
7TIMEOUTRequest timed out before the server responded. Also returned for HTTP 408.

Example:

dw workflow:describe chk-does-not-exist
echo $? # 5 (NOT_FOUND)

dw server:health --server=http://unreachable:9999
echo $? # 3 (NETWORK)

The canonical source is DurableWorkflow\Cli\Support\ExitCode in the CLI repository.

Reference

See the CLI command reference for command shapes, options, output modes, and automation failure behavior.