Your first workflow
Start a runtime, install the published SDK, run its shipped worker and client, and check one durable result. Each stage ends with something you can observe.
What you need
- PHP 8.1 or newer with
ext-json, Composer 2, and two terminal windows. - Docker for the local Server route, or a provisioned Cloud namespace runtime.
ext-pcntlin the worker process soSIGINTandSIGTERMdrain cleanly.
1. Install the published SDK
mkdir durable-php-quickstart
cd durable-php-quickstart
composer init --name=acme/durable-php-quickstart --no-interaction
composer require durable-workflow/sdk:^2.0@RC
composer show durable-workflow/sdk reports a 2.0 prerelease release.
2. Start or select the runtime
Choose one path and keep its values in both terminals.
Self-hosted Server on your machine
Bootstrap a durable volume once, then start the Server version qualified with this SDK:
export DW_SERVER_IMAGE="$(
curl -fsSL https://php.durable-workflow.com/quickstart-contract.json \
| php -r '$contract = json_decode(stream_get_contents(STDIN), true, 512, JSON_THROW_ON_ERROR); echo $contract["runtime_targets"]["server"]["image"];'
)"
export DW_AUTH_TOKEN=dev-token
docker volume create durable-workflow-php-quickstart
docker run --rm \
-v durable-workflow-php-quickstart:/app/database \
-e DW_AUTH_DRIVER=token \
-e DW_AUTH_TOKEN="$DW_AUTH_TOKEN" \
"$DW_SERVER_IMAGE" server-bootstrap
docker run -d --name durable-workflow-php-server \
-p 8080:8080 \
-v durable-workflow-php-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
export DURABLE_WORKFLOW_RUNTIME_URL='http://localhost:8080'
export DURABLE_WORKFLOW_NAMESPACE='default'
export DURABLE_WORKFLOW_CLIENT_TOKEN="$DW_AUTH_TOKEN"
export DURABLE_WORKFLOW_WORKER_TOKEN="$DW_AUTH_TOKEN"
The readiness loop exits successfully. Pass the bare Server origin to the SDK; it appends its own /api segment.
Durable Workflow Cloud
Use the complete namespace runtime URI and namespace returned by provisioning. Do not replace the runtime URI with the Cloud control-plane URL or trim its path prefix.
export DURABLE_WORKFLOW_RUNTIME_URL='https://cloud.example/api/runtime/v1/namespaces/<runtime-id>'
export DURABLE_WORKFLOW_NAMESPACE='<provisioned-namespace>'
read -rsp 'Client credential: ' DURABLE_WORKFLOW_CLIENT_TOKEN; echo
export DURABLE_WORKFLOW_CLIENT_TOKEN
read -rsp 'Worker credential: ' DURABLE_WORKFLOW_WORKER_TOKEN; echo
export DURABLE_WORKFLOW_WORKER_TOKEN
The prompts do not echo credentials. Keep them in process environments or a secret manager, never in source or diagnostics.
3. Choose an isolated task queue
export DURABLE_WORKFLOW_TASK_QUEUE="php-quickstart-$(php -r 'echo bin2hex(random_bytes(8));')"
printf '%s\n' "$DURABLE_WORKFLOW_TASK_QUEUE" prints a non-empty, unique queue name.
4. Copy the shipped files
These are the executable files published under the package's examples/ directory. Save each beside the new project's vendor/ directory.
bootstrap.php — locate Composer without a checkout-specific path
<?php
declare(strict_types=1);
(static function (): void {
$candidates = array_unique([
__DIR__.'/vendor/autoload.php',
dirname(__DIR__).'/vendor/autoload.php',
dirname(__DIR__, 2).'/vendor/autoload.php',
dirname(__DIR__, 3).'/autoload.php',
getcwd().'/vendor/autoload.php',
]);
foreach ($candidates as $candidate) {
if (is_file($candidate)) {
require $candidate;
return;
}
}
throw new RuntimeException(
'Composer autoload.php was not found. Run the example from a Composer project or copy all three example files beside vendor/.',
);
})();
function quickstartEnvironment(string $name): string
{
$value = getenv($name);
if (!is_string($value) || trim($value) === '') {
throw new RuntimeException("Set the {$name} environment variable before running this example.");
}
return trim($value);
}worker.php — discover the attributed workflow and activity before polling
<?php
declare(strict_types=1);
require __DIR__.'/bootstrap.php';
use DurableWorkflow\Attribute\Activity;
use DurableWorkflow\Attribute\Workflow;
use DurableWorkflow\Client;
use DurableWorkflow\Worker;
use DurableWorkflow\Worker\ActivityContext;
use DurableWorkflow\Worker\WorkflowContext;
final class GreeterWorkflow
{
#[Workflow('quickstart.php.greeter')]
public function run(WorkflowContext $context, string $name): array
{
$greeting = $context->activity('quickstart.php.greet', [$name]);
return ['greeting' => $greeting];
}
}
final class GreetingActivities
{
#[Activity('quickstart.php.greet')]
public function greet(ActivityContext $context, string $name): string
{
return "hello, {$name}";
}
}
$client = new Client(
quickstartEnvironment('DURABLE_WORKFLOW_RUNTIME_URL'),
namespace: quickstartEnvironment('DURABLE_WORKFLOW_NAMESPACE'),
workerToken: quickstartEnvironment('DURABLE_WORKFLOW_WORKER_TOKEN'),
);
Worker::create($client, quickstartEnvironment('DURABLE_WORKFLOW_TASK_QUEUE'))
->register(GreeterWorkflow::class, GreetingActivities::class)
->run();client.php — start a unique workflow and wait for its result
<?php
declare(strict_types=1);
require __DIR__.'/bootstrap.php';
use DurableWorkflow\Client;
$client = new Client(
quickstartEnvironment('DURABLE_WORKFLOW_RUNTIME_URL'),
namespace: quickstartEnvironment('DURABLE_WORKFLOW_NAMESPACE'),
controlToken: quickstartEnvironment('DURABLE_WORKFLOW_CLIENT_TOKEN'),
);
$workflowId = 'php-quickstart-'.bin2hex(random_bytes(16));
$handle = $client->startWorkflow(
workflowType: 'quickstart.php.greeter',
workflowId: $workflowId,
taskQueue: quickstartEnvironment('DURABLE_WORKFLOW_TASK_QUEUE'),
input: ['PHP'],
);
$result = $handle->result(timeoutSeconds: 90, pollIntervalSeconds: 1);
echo json_encode(
['workflow_id' => $workflowId, 'result' => $result],
JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES,
).PHP_EOL;5. Run the worker, client, and result read
In terminal one, expose only the worker role credential:
env -u DURABLE_WORKFLOW_CLIENT_TOKEN php worker.php
In terminal two, reuse the runtime, namespace, and task queue but expose only the client role credential:
env -u DURABLE_WORKFLOW_WORKER_TOKEN php client.php
The client prints a fresh php-quickstart-… workflow ID and "result":{"greeting":"hello, PHP"}. Stop the worker with Ctrl+C; the managed worker drains before returning.
What just became durable?
The runtime owns the workflow ID, run history, pending tasks, and terminal result. The PHP worker owns deterministic orchestration and activity code. If the process exits after an activity result is committed, another compatible worker can replay the history and continue without repeating that committed activity.
Next, configure role-specific clients or explore workflows and activities.