Workflow message passing - PHP SDK
How to develop with Signals
A Signal is a message sent to a running Workflow Execution.
Signals are defined in your code and handled in your Workflow Definition. Signals can be sent to Workflow Executions from a Temporal Client or from another Workflow Execution.
How to define a Signal
A Signal has a name and can have arguments.
- The name, also called a Signal type, is a string.
- The arguments must be serializable.
Workflows can answer synchronous Queries and receive Signals.
All interface methods must have one of the following annotations:
- #[WorkflowMethod] indicates an entry point to a Workflow.
It contains parameters that specify timeouts and a Task Queue name.
Required parameters (such as
executionStartToCloseTimeoutSeconds
) that are not specified through the annotation must be provided at runtime. - #[SignalMethod] indicates a method that reacts to external signals. It must have a
void
return type. - #[QueryMethod] indicates a method that reacts to synchronous query requests. It must have a non
void
return type.
It is possible (though not recommended for usability reasons) to annotate concrete class implementation.
You can have more than one method with the same annotation (except #[WorkflowMethod]).
For example:
use Temporal\Workflow\WorkflowInterface;
use Temporal\Workflow\WorkflowMethod;
use Temporal\Workflow\SignalMethod;
use Temporal\Workflow\QueryMethod;
#[WorkflowInterface]
interface FileProcessingWorkflow
{
#[WorkflowMethod]
public function processFile(Argument $args);
#[QueryMethod("history")]
public function getHistory(): array;
#[QueryMethod("status")]
public function getStatus(): string;
#[SignalMethod]
public function retryNow(): void;
#[SignalMethod]
public function abandon(): void;
}
Note that name parameter of Workflow method annotations can be used to specify name of Workflow, Signal and Query types. If name is not specified the short name of the Workflow interface is used.
In the above code the #[WorkflowMethod(name)]
is not specified, thus the Workflow Type defaults to "FileProcessingWorkflow"
.
How to handle a Signal
Workflows listen for Signals by the Signal's name.
Use the #[SignalMethod]
annotation to handle Signals in the Workflow interface:
use Temporal\Workflow;
#[Workflow\WorkflowInterface]
class YourWorkflow
{
private bool $value;
#[Workflow\WorkflowMethod]
public function run()
{
yield Workflow::await(fn()=> $this->value);
return 'OK';
}
#[Workflow\SignalMethod]
public function setValue(bool $value)
{
$this->value = $value;
}
}
In the preceding example, the Workflow updates the protected value.
The main Workflow coroutine waits for the value to change by using the Workflow::await()
function.
How to send a Signal from a Temporal Client
When a Signal is sent successfully from the Temporal Client, the WorkflowExecutionSignaled Event appears in the Event History of the Workflow that receives the Signal.
To send a Signal to a Workflow Execution from a Client, call the Signal method, annotated with #[SignalMethod]
in the Workflow interface, from the Client code.
To send a Signal to a Workflow, use WorkflowClient->newWorkflowStub
or WorkflowClient->newUntypedWorkflowStub
:
$workflow = $workflowClient->newWorkflowStub(YourWorkflow::class);
$run = $workflowClient->start($workflow);
// do something
$workflow->setValue(true);
assert($run->getValue() === true);
Use WorkflowClient->newRunningWorkflowStub
or WorkflowClient->newUntypedRunningWorkflowStub
with Workflow Id to send Signals to already running Workflows.
$workflow = $workflowClient->newRunningWorkflowStub(YourWorkflow::class, 'workflowID');
$workflow->setValue(true);
See Handle Signal for details on how to handle Signals in a Workflow.
How to send a Signal from a Workflow
A Workflow can send a Signal to another Workflow, in which case it's called an External Signal.
When an External Signal is sent:
- A SignalExternalWorkflowExecutionInitiated Event appears in the sender's Event History.
- A WorkflowExecutionSignaled Event appears in the recipient's Event History.
To send Signal to a Workflow use WorkflowClient
->newWorkflowStub
or WorkflowClient
->newUntypedWorkflowStub
:
$workflow = $workflowClient->newWorkflowStub(YourWorkflow::class);
$run = $workflowClient->start($workflow);
// do something
$workflow->setValue(true);
assert($run->getValue() === true);
Use WorkflowClient
->newRunningWorkflowStub
or WorkflowClient->newUntypedRunningWorkflowStub
with Workflow Id to send
Signals to a running Workflow.
$workflow = $workflowClient->newRunningWorkflowStub(YourWorkflow::class, 'workflowID');
$workflow->setValue(true);
How to Signal-With-Start
Signal-With-Start is used from the Client. It takes a Workflow Id, Workflow arguments, a Signal name, and Signal arguments.
If there's a Workflow running with the given Workflow Id, it will be signaled. If there isn't, a new Workflow will be started and immediately signaled.
In cases where you may not know if a Workflow is running, and want to send a Signal to it, use startwithSignal
.
If a running Workflow exists, the startwithSignal
API sends the Signal.
If there is no running Workflow, the API starts a new Workflow Run and delivers the Signal to it.
$workflow = $workflowClient->newWorkflowStub(YourWorkflow::class);
$run = $workflowClient->startWithSignal(
$workflow,
'setValue',
[true], // signal arguments
[] // start arguments
);
How to develop with Queries
A Query is a synchronous operation that is used to get the state of a Workflow Execution.
How to define a Query
A Query has a name and can have arguments.
- The name, also called a Query type, is a string.
- The arguments must be serializable.
Workflows can answer synchronous Queries and receive Signals.
All interface methods must have one of the following annotations:
- #[WorkflowMethod] indicates an entry point to a Workflow.
It contains parameters that specify timeouts and a Task Queue name.
Required parameters (such as
executionStartToCloseTimeoutSeconds
) that are not specified through the annotation must be provided at runtime. - #[SignalMethod] indicates a method that reacts to external signals. It must have a
void
return type. - #[QueryMethod] indicates a method that reacts to synchronous query requests. It must have a non
void
return type.
It is possible (though not recommended for usability reasons) to annotate concrete class implementation.
You can have more than one method with the same annotation (except #[WorkflowMethod]).
For example:
use Temporal\Workflow\WorkflowInterface;
use Temporal\Workflow\WorkflowMethod;
use Temporal\Workflow\SignalMethod;
use Temporal\Workflow\QueryMethod;
#[WorkflowInterface]
interface FileProcessingWorkflow
{
#[WorkflowMethod]
public function processFile(Argument $args);
#[QueryMethod("history")]
public function getHistory(): array;
#[QueryMethod("status")]
public function getStatus(): string;
#[SignalMethod]
public function retryNow(): void;
#[SignalMethod]
public function abandon(): void;
}
Note that name parameter of Workflow method annotations can be used to specify name of Workflow, Signal and Query types. If name is not specified the short name of the Workflow interface is used.
In the above code the #[WorkflowMethod(name)]
is not specified, thus the Workflow Type defaults to "FileProcessingWorkflow"
.
How to handle a Query
Queries are handled by your Workflow.
Don't include any logic that causes Command generation within a Query handler (such as executing Activities). Including such logic causes unexpected behavior.
You can add custom Query types to handle Queries such as Querying the current state of a
Workflow, or Querying how many Activities the Workflow has completed. To do this, you need to set
up a Query handler using method attribute QueryMethod
or Workflow::registerQuery
.
#[Workflow\WorkflowInterface]
class YourWorkflow
{
#[Workflow\QueryMethod]
public function getValue()
{
return 42;
}
#[Workflow\WorkflowMethod]
public function run()
{
// workflow code
}
}
The handler function can receive any number of input parameters, but all input parameters must be
serializable. The following sample code sets up a Query handler that handles the Query type of
currentState
:
#[Workflow\WorkflowInterface]
class YourWorkflow
{
private string $currentState;
#[Workflow\QueryMethod('current_state')]
public function getCurrentState(): string
{
return $this->currentState;
}
#[Workflow\WorkflowMethod]
public function run()
{
// Your normal Workflow code begins here, and you update the currentState
// as the code makes progress.
$this->currentState = 'waiting timer';
try{
yield Workflow::timer(DateInterval::createFromDateString('1 hour'));
} catch (\Throwable $e) {
$this->currentState = 'timer failed';
throw $e;
}
$yourActivity = Workflow::newActivityStub(
YourActivityInterface::class,
ActivityOptions::new()->withScheduleToStartTimeout(60)
);
$this->currentState = 'waiting activity';
try{
yield $yourActivity->doSomething('some input');
} catch (\Throwable $e) {
$this->currentState = 'activity failed';
throw $e;
}
$this->currentState = 'done';
return null;
}
}
You can also issue a Query from code using the QueryWorkflow()
API on a Temporal Client object.
Use WorkflowStub
to Query Workflow instances from your Client code (can be applied to both running and closed Workflows):
$workflow = $workflowClient->newWorkflowStub(
YourWorkflow::class,
WorkflowOptions::new()
);
$workflowClient->start($workflow);
var_dump($workflow->getCurrentState());
sleep(60);
var_dump($workflow->getCurrentState());
How to send a Query
Queries are sent from a Temporal Client.
How to develop with Updates
An Update is an operation that can mutate the state of a Workflow Execution and return a response.
Define Update
How to define an Update using the PHP SDK.
Workflow Updates handlers are methods in your Workflow Definition designed to handle updates. These updates can be triggered during the lifecycle of a Workflow Execution.
An Update handler has a name, arguments, response, and an optional validator.
- The name, also called an Update type, is a string.
- The arguments and response must be serializable.
The #[UpdateMethod]
attribute indicates that the method is used to handle and respond to update requests.
#[UpdateMethod]
public function myUpdate(string $value);
Handle Update
How to handle Updates in a Workflow using the PHP SDK.
Workflows listen for Update by the Update's name.
Use the #[UpdateMethod]
attribute to handle Updates in the Workflow interface.
The handler method can accept multiple serializable input parameters, but it's recommended using only a single parameter.
The function can return a serializable value or void
.
#[WorkflowInterface]
interface FileProcessingWorkflow {
#[WorkflowMethod]
#[ReturnType(ProcessResult::class)]
public function processFile(File $file);
#[UpdateMethod]
public function pauseProcessing(): void;
}
Update handlers, unlike Query handlers, can change Workflow state.
The Updates type defaults to the name of the method.
To overwrite this default naming and assign a custom Update type, use the #[UpdateMethod]
attribute with the name
parameter.
#[WorkflowInterface]
interface FileProcessingWorkflow {
#[WorkflowMethod]
public function processFiles(FileList $files);
#[UpdateMethod(name: 'pause')]
public function pauseProcessing(): void;
}
Register Update Handler dynamically
You can register Update handlers dynamically using the Workflow::registerUpdate()
method.
The third argument is an optional Update validator. The validator function must have the same parameters as the handler and throw an exception if the validation fails.
Workflow::registerUpdate(
name: 'pause',
handler: fn() => $this->paused = true,
validator: fn() => $this->paused === false or throw new \Exception('Workflow is already paused'),
);
Validate Update
How to validate Updates in a Workflow using the PHP SDK.
Validate certain aspects of the data sent to the Workflow using an Update Validator method. For instance, a counter Workflow might never want to accept a non-positive number. Use the #[UpdateValidatorMethod]
attribute and set the forUpdate
argument to the name of your Update handler. Your Update Validator should accept the same input parameters as your Update Handler and return void
.
#[WorkflowInterface]
interface GreetingWorkflow {
#[WorkflowMethod]
public function getGreetings(): array;
#[UpdateMethod]
public function addGreeting(string $name): int;
#[UpdateValidatorMethod(forUpdate: 'addGreeting')]
public function addGreetingValidator(string $name): void;
}
Send Update from a Client
How to send an Update to a Workflow Execution from a Temporal Client using the PHP SDK.
To send an Update to a Workflow Execution from a Client, call the Update method, annotated with #[UpdateMethod]
in the Workflow interface, from the Client code.
In the following Client code example, start the Workflow getGreetings
and call the Update method addGreeting
that is handled in the Workflow.
// Create a typed Workflow stub for GreetingsWorkflow
$workflow = $workflowClient->newWorkflowStub(GreetingWorkflow::class, $workflowOptions);
// Start the Workflow
$run = $workflowClient->start($workflow);
// Send an update to the Workflow. addGreeting returns
// the number of greetings our workflow has received.
$count = $workflow->addGreeting("World");
Async accept
In Workflow Update methods, all Workflow features are available, such as executing Activities and child Workflows, and waiting on timers/conditions.
In cases where it's known that the update will take a long time to execute, or you are not interested in the outcome of its execution, you can use the stub method startUpdate
and move on immediately after receiving the validation result.
Note that the processing Workflow Worker must be available.
Otherwise, the request may block indefinitely or fail due to a timeout.
use Ramsey\Uuid\UuidInterface;
use Temporal\Client\Update\UpdateOptions;
use Temporal\Client\Update\WaitPolicy;
use Temporal\Client\Update\LifecycleStage;
// Create an untyped Workflow stub for GreetingsWorkflow
$stub = $client->newUntypedWorkflowStub('GreetingWorkflow', $workflowOptions);
// Start the Workflow
$run = $client->start($stub);
// Send an Update to the Workflow. UpdateHandle returns
$handle = $stub->startUpdate('addGreeting', 'World');
// Use the UpdateHandle to get the Update result with timeout 2.5 seconds
$result = $handle->getResult(timeout: 2.5);
// You can get more control using UpdateOptions
$resultUuid = $stub->startUpdate(
UpdateOptions::new('storeGreetings', LifecycleStage::StageCompleted)
->withResultType(UuidInterface::class)
)->getResult();
Message handler patterns
This section covers common write operations, such as Signal and Update handlers. It doesn't apply to pure read operations, like Queries or Update Validators.
For additional information, see Inject work into the main Workflow, Ensuring your messages are processed exactly once, and this sample demonstrating safe async
message handling.
Add wait conditions to block
Sometimes, async Signal or Update handlers need to meet certain conditions before they should continue.
You can use a wait condition (Workflow::await()
) to set a function that prevents the code from proceeding until the condition returns true
.
This is an important feature that helps you control your handler logic.
Here are two important use cases for Workflow::await()
:
- Waiting in a handler until it is appropriate to continue.
- Waiting in the main Workflow until all active handlers have finished.
The condition state you're waiting for can be updated by and reflect any part of the Workflow code.
This includes the main Workflow method, other handlers, or child coroutines spawned by the main Workflow method (see Workflow::async()
.
Use wait conditions in handlers
It's common to use a Workflow wait condition to wait until a handler should start.
You can also use wait conditions anywhere else in the handler to wait for a specific condition to become true
.
This allows you to write handlers that pause at multiple points, each time waiting for a required condition to become true
.
Consider a readyForUpdateToExecute
method that runs before your Update handler executes.
The Workflow::await
method waits until your condition is met:
#[UpdateMethod]
public function myUpdate(UpdateInput $input)
{
yield Workflow::await(
fn() => $this->readyForUpdateToExecute($input),
);
// ...
}
Remember: Handlers can execute before the main Workflow method starts.
Ensure your handlers finish before the Workflow completes
Workflow wait conditions can ensure your handler completes before a Workflow finishes.
When your Workflow uses async Signal or Update handlers, your main Workflow method can return or continue-as-new while a handler is still waiting on an async task, such as an Activity result.
The Workflow completing may interrupt the handler before it finishes crucial work and cause client errors when trying retrieve Update results.
Use Workflow::await()
and Workflow::allHandlersFinished()
to address this problem and allow your Workflow to end smoothly:
#[WorkflowInterface]
class MyWorkflow
{
#[WorkflowMethod]
public function run()
{
// ...
yield Workflow::await(fn() => Workflow::allHandlersFinished());
return "workflow-result";
}
}
By default, your Worker will log a warning when you allow a Workflow Execution to finish with unfinished handler executions.
You can silence these warnings on a per-handler basis by passing the unfinishedPolicy
argument to the UpdateMethod
/ SignalMethod
attribute:
#[UpdateMethod(unfinishedPolicy: HandlerUnfinishedPolicy::Abandon)]
public function myUpdate()
{
// ...
}
See Finishing handlers before the Workflow completes for more information.
Use Mutex
to prevent concurrent handler execution
Concurrent processes can interact in unpredictable ways. Incorrectly written concurrent message-passing code may not work correctly when multiple handler instances run simultaneously. Here's an example of a pathological case:
#[WorkflowInterface]
class MyWorkflow
{
// ...
#[SignalMethod]
public function badAsyncHandler()
{
$data = yield Workflow::executeActivity(
type: 'fetch_data',
args: ['url' => 'http://example.com'],
options: ActivityOptions::new()->withStartToCloseTimeout('10 seconds'),
);
$this->x = $data->x;
# 🐛🐛 Bug!! If multiple instances of this handler are executing concurrently, then
# there may be times when the Workflow has $this->x from one Activity execution and $this->y from another.
yield Workflow::timer(1); # or await anything else
$this->y = $data->y;
}
}
Coordinating access using Mutex
corrects this code.
Locking makes sure that only one handler instance can execute a specific section of code at any given time:
use Temporal\Workflow;
#[Workflow\WorkflowInterface]
class MyWorkflow
{
// ...
private Workflow\Mutex $mutex;
public function __construct()
{
$this->mutex = new Workflow\Mutex();
}
#[Workflow\SignalMethod]
public function safeAsyncHandler()
{
$data = yield Workflow::executeActivity(
type: 'fetch_data',
args: ['url' => 'http://example.com'],
options: ActivityOptions::new()->withStartToCloseTimeout('10 seconds'),
);
yield Workflow::runLocked($this->mutex, function () use ($data) {
$this->x = $data->x;
# ✅ OK: the scheduler may switch now to a different handler execution, or to the main workflow
# method, but no other execution of this handler can run until this execution finishes.
yield Workflow::timer(1); # or await anything else
$this->y = $data->y;
});
}
Message handler troubleshooting
When sending a Signal, Update, or Query to a Workflow, your Client might encounter the following errors:
-
The client can't contact the server: You'll receive a
ServiceClientException
in case of a server connection error. How to configure RPC Retry Policy -
RPC timout: You'll receive a
TimeoutException
in case of an RPC timeout. How to configure RPC timeout -
The workflow does not exist: You'll receive a
WorkflowNotFoundException
exception.
See Exceptions in message handlers for a non–PHP-specific discussion of this topic.
Problems when sending a Signal
When using Signal, the only exception that will result from your requests during its execution is ServiceClientException
.
All handlers may experience additional exceptions during the initial (pre-Worker) part of a handler request lifecycle.
For Queries and Updates, the client waits for a response from the Worker. If an issue occurs during the handler Execution by the Worker, the client may receive an exception.
Problems when sending an Update
When working with Updates, you may encounter these errors:
-
No Workflow Workers are polling the Task Queue: Your request will be retried by the SDK Client indefinitely. You can configure RPC timeout to impose a timeout. This raises a
WorkflowUpdateRPCTimeoutOrCanceledException
. -
Update failed: You'll receive a
WorkflowUpdateException
exception. There are two ways this can happen:-
The Update was rejected by an Update validator defined in the Workflow alongside the Update handler.
-
The Update failed after having been accepted.
-
Update failures are like Workflow failures. Issues that cause a Workflow failure in the main method also cause Update failures in the Update handler. These might include:
-
A failed Child Workflow
-
A failed Activity (if the Activity retries have been set to a finite number)
-
The Workflow author raising
ApplicationFailure
-
The handler caused the Workflow Task to fail: A Workflow Task Failure causes the server to retry Workflow Tasks indefinitely. What happens to your Update request depends on its stage:
- If the request hasn't been accepted by the server, you receive a
WorkflowUpdateException
. - If the request has been accepted, it is durable.
Once the Workflow is healthy again after a code deploy, use an
UpdateHandle
to fetch the Update result.
- If the request hasn't been accepted by the server, you receive a
-
The Workflow finished while the Update handler execution was in progress: You'll receive a
WorkflowUpdateException
. This happens if the Workflow finished while the Update handler execution was in progress, for example because-
The Workflow was canceled or failed.
-
The Workflow completed normally or continued-as-new and the Workflow author did not wait for handlers to be finished.
-
Problems when sending a Query
When working with Queries, you may encounter these errors:
-
There is no Workflow Worker polling the Task Queue: You'll receive a
WorkflowNotFoundException
. -
Query failed: You'll receive a
WorkflowQueryException
if something goes wrong during a Query. Any exception in a Query handler will trigger this error. This differs from Signal and Update requests, where exceptions can lead to Workflow Task Failure instead. -
The handler caused the Workflow Task to fail. This would happen, for example, if the Query handler blocks the thread for too long without yielding.