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 (defaultdw).
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.
| Code | Name | Meaning |
|---|---|---|
0 | SUCCESS | Operation completed successfully. |
1 | FAILURE | Generic failure — command ran but did not succeed. |
2 | INVALID | Invalid usage — bad arguments, unknown options, or local validation. Also returned for HTTP 4xx responses that are not covered below (e.g. 400, 422). |
3 | NETWORK | Could not reach the server (connection refused, DNS failure, TLS handshake failure, transport error). |
4 | AUTH | Authentication or authorization failure. Returned for HTTP 401 and 403. |
5 | NOT_FOUND | Resource not found. Returned for HTTP 404. |
6 | SERVER | Server error. Returned for HTTP 5xx. |
7 | TIMEOUT | Request 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.