|
| 1 | +--- |
| 2 | +sidebar_position: 6 |
| 3 | +title: Workflow Authoring API Reference |
| 4 | +description: Reference for the stable v2 PHP workflow authoring facade, durable waits, metadata upserts, and message streams. |
| 5 | +tags: |
| 6 | + - reference |
| 7 | + - workflows |
| 8 | + - php |
| 9 | + - v2 |
| 10 | +keywords: |
| 11 | + - Workflow::activity |
| 12 | + - Workflow::await |
| 13 | + - Workflow::inbox |
| 14 | + - MessageStream |
| 15 | + - PHP workflow authoring API |
| 16 | +--- |
| 17 | + |
| 18 | +# Workflow Authoring API Reference |
| 19 | + |
| 20 | +The v2 PHP authoring API is the code surface that runs inside a workflow |
| 21 | +fiber. Application workflows extend `Workflow\V2\Workflow`, implement a |
| 22 | +`handle()` entry method, and use either the static `Workflow::...` facade or |
| 23 | +the equivalent `Workflow\V2\...` helper functions for durable commands. |
| 24 | + |
| 25 | +This page is a signature reference. For narrative guidance, see |
| 26 | +[Workflows](./workflows.md), [Activities](./activities.md), |
| 27 | +[Signals](../features/signals.md), and [Message Streams](../features/message-streams.md). |
| 28 | + |
| 29 | +## Base Workflow Class |
| 30 | + |
| 31 | +```php |
| 32 | +use Workflow\V2\Workflow; |
| 33 | + |
| 34 | +abstract class Workflow |
| 35 | +{ |
| 36 | + public ?string $connection = null; |
| 37 | + public ?string $queue = null; |
| 38 | + |
| 39 | + public function workflowId(): string; |
| 40 | + public function runId(): string; |
| 41 | + public function lastChild(): ?ChildWorkflowHandle; |
| 42 | + public function children(): array; |
| 43 | + public function historyLength(): int; |
| 44 | + public function historySize(): int; |
| 45 | + public function shouldContinueAsNew(): bool; |
| 46 | +} |
| 47 | +``` |
| 48 | + |
| 49 | +| Member | Use when | Return contract | |
| 50 | +| --- | --- | --- | |
| 51 | +| `workflowId()` | The workflow needs its stable public instance id. | Instance id string, unchanged across continue-as-new. | |
| 52 | +| `runId()` | The workflow needs the currently executing run id. | Run id string for the selected execution. | |
| 53 | +| `lastChild()` | The workflow needs to signal the most recently spawned child. | `ChildWorkflowHandle` or `null`. | |
| 54 | +| `children()` | The workflow needs handles for children visible to the current replay sequence. | List of `ChildWorkflowHandle`. | |
| 55 | +| `historyLength()` | The workflow needs a count-based history budget signal. | Current history event count. | |
| 56 | +| `historySize()` | The workflow needs a byte-based history budget signal. | Approximate persisted history size in bytes. | |
| 57 | +| `shouldContinueAsNew()` | The workflow should rotate before history becomes expensive. | `true` when configured history budgets recommend rotation. | |
| 58 | + |
| 59 | +`workflowId()` is the public address for signals, updates, queries, and message |
| 60 | +streams. `runId()` is useful for diagnostics and selected-run tooling, but most |
| 61 | +authoring code should keep external callers on the instance id. |
| 62 | + |
| 63 | +## Durable Commands |
| 64 | + |
| 65 | +The static facade delegates to namespaced helpers in `Workflow\V2`. The two |
| 66 | +forms are equivalent: |
| 67 | + |
| 68 | +```php |
| 69 | +use Workflow\V2\Workflow; |
| 70 | +use function Workflow\V2\activity; |
| 71 | + |
| 72 | +$resultFromFacade = Workflow::activity(SendReceipt::class, $orderId); |
| 73 | +$resultFromHelper = activity(SendReceipt::class, $orderId); |
| 74 | +``` |
| 75 | + |
| 76 | +| Facade | Helper | Signature | Durable effect | |
| 77 | +| --- | --- | --- | --- | |
| 78 | +| `Workflow::activity()` | `activity()` | `activity(string $activity, mixed ...$arguments): mixed` | Schedules an activity and waits for its result. | |
| 79 | +| `Workflow::executeActivity()` | `activity()` | `executeActivity(string $activity, mixed ...$arguments): mixed` | Alias for `activity()`. | |
| 80 | +| `Workflow::child()` | `child()` | `child(string $workflow, mixed ...$arguments): mixed` | Starts a child workflow and waits for its result. | |
| 81 | +| `Workflow::executeChildWorkflow()` | `child()` | `executeChildWorkflow(string $workflow, mixed ...$arguments): mixed` | Alias for `child()`. | |
| 82 | +| `Workflow::async()` | `async()` | `async(callable $callback): mixed` | Runs a callable as an auto-generated child workflow. | |
| 83 | +| `Workflow::all()` | `all()` | `all(iterable $calls): mixed` | Waits for concurrent calls and returns results in iteration order. | |
| 84 | +| `Workflow::parallel()` | `all()` | `parallel(iterable $calls): mixed` | Alias for `all()`. | |
| 85 | +| `Workflow::await()` | `await()` | `await(callable|string $condition, int|string|CarbonInterval|null $timeout = null, ?string $conditionKey = null): mixed` | Waits for a named signal or replay-safe condition. | |
| 86 | +| `Workflow::awaitWithTimeout()` | `await()` | `awaitWithTimeout(int|string|CarbonInterval $timeout, callable|string $condition, ?string $conditionKey = null): mixed` | Waits for a signal or condition with an explicit timeout. | |
| 87 | +| `Workflow::awaitSignal()` | `await()` | `awaitSignal(string $name): mixed` | Waits for a named signal. | |
| 88 | +| `Workflow::timer()` | `timer()` | `timer(int|string|CarbonInterval $duration): mixed` | Suspends until durable time advances. | |
| 89 | +| `Workflow::sideEffect()` | `sideEffect()` | `sideEffect(callable $callback): mixed` | Records a non-deterministic result in history and replays it. | |
| 90 | +| `Workflow::uuid4()` | `uuid4()` | `uuid4(): mixed` | Generates a replay-stable UUIDv4. | |
| 91 | +| `Workflow::uuid7()` | `uuid7()` | `uuid7(): mixed` | Generates a replay-stable UUIDv7. | |
| 92 | +| `Workflow::continueAsNew()` | `continueAsNew()` | `continueAsNew(mixed ...$arguments): mixed` | Ends the current run and starts a new run for the same instance. | |
| 93 | +| `Workflow::getVersion()` | `getVersion()` | `getVersion(string $changeId, int $minSupported = WorkflowStub::DEFAULT_VERSION, int $maxSupported = 1): mixed` | Negotiates a replay-safe workflow-code version. | |
| 94 | +| `Workflow::patched()` | `patched()` | `patched(string $changeId): mixed` | Returns whether the run crossed a named patch marker. | |
| 95 | +| `Workflow::deprecatePatch()` | `deprecatePatch()` | `deprecatePatch(string $changeId): mixed` | Keeps a patch marker alive after old code is removed. | |
| 96 | +| `Workflow::upsertMemo()` | `upsertMemo()` | `upsertMemo(array $entries): void` | Updates non-indexed run metadata. | |
| 97 | +| `Workflow::upsertSearchAttributes()` | `upsertSearchAttributes()` | `upsertSearchAttributes(array $attributes): void` | Updates indexed operator-visible metadata. | |
| 98 | +| `Workflow::now()` | `now()` | `now(): CarbonInterface` | Reads deterministic workflow time. | |
| 99 | + |
| 100 | +```php |
| 101 | +use Workflow\V2\Workflow; |
| 102 | + |
| 103 | +final class FulfillmentWorkflow extends Workflow |
| 104 | +{ |
| 105 | + public function handle(string $orderId): array |
| 106 | + { |
| 107 | + Workflow::upsertSearchAttributes([ |
| 108 | + 'order_id' => $orderId, |
| 109 | + 'status' => 'packing', |
| 110 | + ]); |
| 111 | + |
| 112 | + $label = Workflow::activity(CreateShippingLabel::class, $orderId); |
| 113 | + Workflow::timer('15 minutes'); |
| 114 | + $receipt = Workflow::activity(SendReceipt::class, $orderId, $label); |
| 115 | + |
| 116 | + return [ |
| 117 | + 'receipt' => $receipt, |
| 118 | + 'workflow_id' => $this->workflowId(), |
| 119 | + 'run_id' => $this->runId(), |
| 120 | + ]; |
| 121 | + } |
| 122 | +} |
| 123 | +``` |
| 124 | + |
| 125 | +## Timer Helpers |
| 126 | + |
| 127 | +Timer helpers are shorthand for `timer()` and `Workflow::timer()`: |
| 128 | + |
| 129 | +```php |
| 130 | +use Workflow\V2\Workflow; |
| 131 | + |
| 132 | +Workflow::seconds(30); |
| 133 | +Workflow::minutes(5); |
| 134 | +Workflow::hours(2); |
| 135 | +Workflow::days(1); |
| 136 | +Workflow::weeks(1); |
| 137 | +Workflow::months(1); |
| 138 | +Workflow::years(1); |
| 139 | +``` |
| 140 | + |
| 141 | +| Helper | Equivalent | |
| 142 | +| --- | --- | |
| 143 | +| `seconds(int $seconds)` | `timer($seconds)` | |
| 144 | +| `minutes(int $minutes)` | `timer($minutes * 60)` | |
| 145 | +| `hours(int $hours)` | `timer($hours * 3600)` | |
| 146 | +| `days(int $days)` | `timer($days * 86400)` | |
| 147 | +| `weeks(int $weeks)` | `timer($weeks * 604800)` | |
| 148 | +| `months(int $months)` | `timer("{$months} months")` | |
| 149 | +| `years(int $years)` | `timer("{$years} years")` | |
| 150 | + |
| 151 | +Use explicit `timer()` calls when the duration comes from configuration or |
| 152 | +workflow input. Use timer helpers when the source code should read as a fixed |
| 153 | +business wait. |
| 154 | + |
| 155 | +## Message Streams |
| 156 | + |
| 157 | +Open durable message streams from the workflow instance: |
| 158 | + |
| 159 | +```php |
| 160 | +use Workflow\V2\MessageStream; |
| 161 | +use Workflow\V2\Workflow; |
| 162 | + |
| 163 | +final class AssistantWorkflow extends Workflow |
| 164 | +{ |
| 165 | + public function handle(string $targetWorkflowId): array |
| 166 | + { |
| 167 | + $message = $this->inbox('ai.user')->receiveOne(); |
| 168 | + |
| 169 | + if ($message === null) { |
| 170 | + return ['status' => 'waiting']; |
| 171 | + } |
| 172 | + |
| 173 | + $reply = $this->outbox('ai.assistant')->sendReference( |
| 174 | + targetInstanceId: $targetWorkflowId, |
| 175 | + payloadReference: 'app://payloads/reply-123', |
| 176 | + correlationId: $this->workflowId(), |
| 177 | + idempotencyKey: 'reply-123', |
| 178 | + metadata: ['kind' => 'assistant_reply'], |
| 179 | + ); |
| 180 | + |
| 181 | + return [ |
| 182 | + 'status' => 'sent', |
| 183 | + 'stream' => $reply->stream_key, |
| 184 | + 'sequence' => $reply->sequence, |
| 185 | + ]; |
| 186 | + } |
| 187 | +} |
| 188 | +``` |
| 189 | + |
| 190 | +| Method | Signature | Contract | |
| 191 | +| --- | --- | --- | |
| 192 | +| `$this->messages()` | `messages(?string $streamKey = null, ?MessageService $messages = null): MessageStream` | Opens the stream for reading or sending. | |
| 193 | +| `$this->inbox()` | `inbox(?string $streamKey = null, ?MessageService $messages = null): MessageStream` | Alias for inbound authoring code. | |
| 194 | +| `$this->outbox()` | `outbox(?string $streamKey = null, ?MessageService $messages = null): MessageStream` | Alias for outbound authoring code. | |
| 195 | +| `MessageStream::key()` | `key(): string` | Returns the stream key. | |
| 196 | +| `MessageStream::cursor()` | `cursor(): int` | Returns the durable cursor position for this run. | |
| 197 | +| `MessageStream::hasPending()` | `hasPending(): bool` | Returns whether unconsumed messages exist on the stream. | |
| 198 | +| `MessageStream::pendingCount()` | `pendingCount(): int` | Returns the number of unconsumed messages on the stream. | |
| 199 | +| `MessageStream::peek()` | `peek(int $limit = 100): Collection` | Reads pending messages without consuming them. | |
| 200 | +| `MessageStream::receive()` | `receive(int $limit = 1, ?int $consumedBySequence = null): Collection` | Reads and consumes messages, recording cursor advancement. | |
| 201 | +| `MessageStream::receiveOne()` | `receiveOne(?int $consumedBySequence = null): ?WorkflowMessage` | Reads and consumes one message. | |
| 202 | +| `MessageStream::sendReference()` | `sendReference(string $targetInstanceId, ?string $payloadReference = null, MessageChannel|string $channel = MessageChannel::WorkflowMessage, ?string $correlationId = null, ?string $idempotencyKey = null, array $metadata = [], ?DateTimeInterface $expiresAt = null): WorkflowMessage` | Sends an ordered payload-reference message to another workflow instance. | |
| 203 | + |
| 204 | +Use message streams for repeated ordered messages with cursor semantics. Use |
| 205 | +[Signals](../features/signals.md) for one-shot external events and |
| 206 | +[Updates](../features/updates.md) for request/return mutations. |
| 207 | + |
| 208 | +## Attributes And Contracts |
| 209 | + |
| 210 | +```php |
| 211 | +use Workflow\QueryMethod; |
| 212 | +use Workflow\UpdateMethod; |
| 213 | +use Workflow\V2\Attributes\Signal; |
| 214 | +use Workflow\V2\Attributes\Type; |
| 215 | +use Workflow\V2\Workflow; |
| 216 | + |
| 217 | +#[Type('order-approval')] |
| 218 | +#[Signal('approved-by', [ |
| 219 | + ['name' => 'approvedBy', 'type' => 'string', 'allows_null' => false], |
| 220 | +])] |
| 221 | +final class OrderApprovalWorkflow extends Workflow |
| 222 | +{ |
| 223 | + private string $stage = 'waiting'; |
| 224 | + |
| 225 | + public function handle(): void |
| 226 | + { |
| 227 | + $this->stage = Workflow::awaitSignal('approved-by'); |
| 228 | + } |
| 229 | + |
| 230 | + #[QueryMethod('current-stage')] |
| 231 | + public function currentStage(): string |
| 232 | + { |
| 233 | + return $this->stage; |
| 234 | + } |
| 235 | + |
| 236 | + #[UpdateMethod('mark-ready')] |
| 237 | + public function markReady(): string |
| 238 | + { |
| 239 | + return $this->stage = 'ready'; |
| 240 | + } |
| 241 | +} |
| 242 | +``` |
| 243 | + |
| 244 | +| Attribute | Target | Stable contract | |
| 245 | +| --- | --- | --- | |
| 246 | +| `#[Type('type-key')]` | Workflow or activity class | Declares the language-neutral durable type key. | |
| 247 | +| `#[Signal('signal-name', [...])]` | Workflow class, repeatable | Declares accepted signal names and optional ordered parameter contracts. | |
| 248 | +| `#[QueryMethod('query-name')]` | Workflow method | Declares a replay-safe query name. Omit the name to use the PHP method name. | |
| 249 | +| `#[UpdateMethod('update-name')]` | Workflow method | Declares a replay-safe update name. Omit the name to use the PHP method name. | |
| 250 | + |
| 251 | +Signals, queries, and updates are public workflow contracts. Prefer explicit |
| 252 | +names so PHP method renames do not become API breaks. |
| 253 | + |
| 254 | +## Failure Surface |
| 255 | + |
| 256 | +Authoring API failures are durable workflow failures unless the command is |
| 257 | +rejected before execution: |
| 258 | + |
| 259 | +| Surface | Typical failure | Operator meaning | |
| 260 | +| --- | --- | --- | |
| 261 | +| `activity()` | Activity throws, times out, or exhausts retry policy. | The run records activity failure history and follows workflow error handling. | |
| 262 | +| `child()` | Child workflow fails, cancels, terminates, or times out. | The parent observes a child failure outcome at the waiting command. | |
| 263 | +| `await()` | Timeout elapses before the condition or signal is satisfied. | The wait returns or fails according to the selected await form. | |
| 264 | +| `timer()` | Invalid duration input after normalization. | Authoring code should pass positive durations or explicit zero-duration waits. | |
| 265 | +| `continueAsNew()` | New run cannot be created. | Current run remains the evidence point for the failed transition. | |
| 266 | +| `upsertSearchAttributes()` | Attribute key, count, or total size exceeds limits. | The run fails before invalid indexed metadata is persisted. | |
| 267 | +| `MessageStream::receive()` | No positive workflow history sequence is available. | Receive must occur from workflow execution, not direct service code. | |
| 268 | +| `MessageStream::sendReference()` | Payload reference, route, or storage contract is invalid downstream. | Message ordering remains separate from payload-store integrity. | |
| 269 | + |
| 270 | +For payload and history limits, see [Structural Limits](../constraints/structural-limits.md). |
| 271 | +For command rejection responses outside PHP workflow code, see |
| 272 | +[Server API Reference](../polyglot/server-api-reference.md). |
| 273 | + |
| 274 | +## Determinism Rules |
| 275 | + |
| 276 | +Workflow code must be replay-safe. Keep irreversible or non-deterministic work |
| 277 | +behind durable commands: |
| 278 | + |
| 279 | +```php |
| 280 | +use Workflow\V2\Workflow; |
| 281 | + |
| 282 | +final class DeterministicWorkflow extends Workflow |
| 283 | +{ |
| 284 | + public function handle(): array |
| 285 | + { |
| 286 | + $workflowTime = Workflow::now(); |
| 287 | + $stableId = Workflow::uuid7(); |
| 288 | + $remoteQuote = Workflow::activity(FetchQuote::class); |
| 289 | + |
| 290 | + return [ |
| 291 | + 'time' => $workflowTime->toIso8601String(), |
| 292 | + 'id' => $stableId, |
| 293 | + 'quote' => $remoteQuote, |
| 294 | + ]; |
| 295 | + } |
| 296 | +} |
| 297 | +``` |
| 298 | + |
| 299 | +- Use `Workflow::now()` instead of wall-clock time in workflow branches. |
| 300 | +- Use `Workflow::uuid4()` or `Workflow::uuid7()` instead of direct randomness. |
| 301 | +- Put network calls, filesystem writes, email sends, and external side effects |
| 302 | + in activities. |
| 303 | +- Use `Workflow::sideEffect()` only when the value must be captured in history |
| 304 | + and the side effect itself is not the business action. |
| 305 | +- Use `Workflow::getVersion()`, `Workflow::patched()`, and |
| 306 | + `Workflow::deprecatePatch()` to evolve workflow code without breaking replay. |
0 commit comments