Skip to main content

Features - PHP SDK feature guide

The Features section of the Temporal Developer's guide provides basic implementation guidance on how to use many of the development features available to Workflows and Activities in the Temporal Platform.

In this section you can find the following:

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:

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::registerQueryHandler.

#[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.

How to define an Update

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. 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 argument.

#[UpdateMethod(name: 'my_update')]
public function myUpdate(string $signalName);

How to handle Updates in a Workflow

Workflows listen for Update by the Update's name.

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]
public function processFiles(FileList $files);

#[UpdateMethod(name: 'pause')]
public function pauseProcessing(): void;

#[UpdateMethod]
#[ReturnType(AddResult::class)]
public function addFile(FileInfo $file);
}

Update handlers, unlike Query handlers, can change Workflow state.

How to validate an Update in a Workflow

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;
}

How to send an Update from a Temporal Client

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.

use Ramsey\Uuid\UuidInterface;
use Temporal\Client\Update\UpdateOptions;
use Temporal\Client\Update\WaitPolicy;
use Temporal\Client\Update\LifecycleStage;

// Create a typed 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')
->withWaitPolicy(WaitPolicy::new()->withLifecycleStage(LifecycleStage::StageCompleted))
->withResultType(UuidInterface::class)
)->getResult();

Workflow timeouts

Each Workflow timeout controls the maximum duration of a different aspect of a Workflow Execution.

Workflow timeouts are set when starting the Workflow Execution.

Create an instance of WorkflowOptions in the Client code and set your timeout.

Available timeouts are:

  • withWorkflowExecutionTimeout()
  • withWorkflowRunTimeout()
  • withWorkflowTaskTimeout()
$workflow = $this->workflowClient->newWorkflowStub(
DynamicSleepWorkflowInterface::class,
WorkflowOptions::new()
->withWorkflowId(DynamicSleepWorkflow::WORKFLOW_ID)
->withWorkflowIdReusePolicy(WorkflowIdReusePolicy::WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE)
// Set Workflow Timeout duration
->withWorkflowExecutionTimeout(CarbonInterval::minutes(2))
// ->withWorkflowRunTimeout(CarbonInterval::minute(2))
// ->withWorkflowTaskTimeout(CarbonInterval::minute(2))
);

Workflow retries

A Retry Policy can work in cooperation with the timeouts to provide fine controls to optimize the execution experience.

Use a Retry Policy to retry a Workflow Execution in the event of a failure.

Workflow Executions do not retry by default, and Retry Policies should be used with Workflow Executions only in certain situations.

A Retry Policy can be configured with an instance of the RetryOptions object. To enable retries for a Workflow, you need to provide a Retry Policy object via ChildWorkflowOptions for Child Workflows or via WorkflowOptions for top-level Workflows.

$workflow = $this->workflowClient->newWorkflowStub(
CronWorkflowInterface::class,
WorkflowOptions::new()->withRetryOptions(
RetryOptions::new()->withInitialInterval(120)
)
);

How to set Activity timeouts

Each Activity timeout controls the maximum duration of a different aspect of an Activity Execution.

The following timeouts are available in the Activity Options.

An Activity Execution must have either the Start-To-Close or the Schedule-To-Close Timeout set.

Because Activities are reentrant, only a single stub can be used for multiple Activity invocations.

Available timeouts are:

  • withScheduleToCloseTimeout()
  • withStartToCloseTimeout()
  • withScheduleToStartTimeout()
$this->greetingActivity = Workflow::newActivityStub(
GreetingActivityInterface::class,
// Set Activity Timeout duration
ActivityOptions::new()
->withScheduleToCloseTimeout(CarbonInterval::seconds(2))
// ->withStartToCloseTimeout(CarbonInterval::seconds(2))
// ->withScheduleToStartTimeout(CarbonInterval::seconds(10))
);

How to set an Activity Retry Policy

A Retry Policy works in cooperation with the timeouts to provide fine controls to optimize the execution experience.

Activity Executions are automatically associated with a default Retry Policy if a custom one is not provided.

To set an Activity Retry, set {@link RetryOptions} on {@link ActivityOptions}. The follow example creates a new Activity with the given options.

$this->greetingActivity = Workflow::newActivityStub(
GreetingActivityInterface::class,
ActivityOptions::new()
->withScheduleToCloseTimeout(CarbonInterval::seconds(10))
->withRetryOptions(
RetryOptions::new()
->withInitialInterval(CarbonInterval::seconds(1))
->withMaximumAttempts(5)
->withNonRetryableExceptions([\InvalidArgumentException::class])
)
);
}

For an executable code sample, see ActivityRetry sample in the PHP samples repository.

How to Heartbeat an Activity

An Activity Heartbeat is a ping from the Worker Process that is executing the Activity to the Temporal Cluster. Each Heartbeat informs the Temporal Cluster that the Activity Execution is making progress and the Worker has not crashed. If the Cluster does not receive a Heartbeat within a Heartbeat Timeout time period, the Activity will be considered failed and another Activity Task Execution may be scheduled according to the Retry Policy.

Heartbeats may not always be sent to the Cluster—they may be throttled by the Worker.

Activity Cancellations are delivered to Activities from the Cluster when they Heartbeat. Activities that don't Heartbeat can't receive a Cancellation. Heartbeat throttling may lead to Cancellation getting delivered later than expected.

Heartbeats can contain a details field describing the Activity's current progress. If an Activity gets retried, the Activity can access the details from the last Heartbeat that was sent to the Cluster.

Some Activities are long-running. To react to a crash quickly, use the Heartbeat mechanism, Activity::heartbeat(), which lets the Temporal Server know that the Activity is still alive. This acts as a periodic checkpoint mechanism for the progress of an Activity.

You can piggyback details on an Activity Heartbeat. If an Activity times out, the last value of details is included in the TimeoutFailure delivered to a Workflow. Then the Workflow can pass the details to the next Activity invocation. Additionally, you can access the details from within an Activity via Activity::getHeartbeatDetails. When an Activity is retried after a failure getHeartbeatDetails enables you to get the value from the last successful Heartbeat.

use Temporal\Activity;

class FileProcessingActivitiesImpl implements FileProcessingActivities
{
// ...
public function download(
string $bucketName,
string $remoteName,
string $localName
): void
{
$this->dowloader->downloadWithProgress(
$bucketName,
$remoteName,
$localName,
// on progress
function ($progress) {
Activity::heartbeat($progress);
}
);

Activity::heartbeat(100); // download complete

// ...
}

// ...
}

How to set a Heartbeat Timeout

A Heartbeat Timeout works in conjunction with Activity Heartbeats.

Some Activities are long-running. To react to a crash quickly, use the Heartbeat mechanism, Activity::heartbeat(), which lets the Temporal Server know that the Activity is still alive. This acts as a periodic checkpoint mechanism for the progress of an Activity.

You can piggyback details on an Activity Heartbeat. If an Activity times out, the last value of details is included in the TimeoutFailure delivered to a Workflow. Then the Workflow can pass the details to the next Activity invocation. Additionally, you can access the details from within an Activity via Activity::getHeartbeatDetails. When an Activity is retried after a failure getHeartbeatDetails enables you to get the value from the last successful Heartbeat.

use Temporal\Activity;

class FileProcessingActivitiesImpl implements FileProcessingActivities
{
// ...
public function download(
string $bucketName,
string $remoteName,
string $localName
): void
{
$this->dowloader->downloadWithProgress(
$bucketName,
$remoteName,
$localName,
// on progress
function ($progress) {
Activity::heartbeat($progress);
}
);

Activity::heartbeat(100); // download complete

// ...
}

// ...
}

How to asynchronously complete an Activity

Asynchronous Activity Completion enables the Activity Function to return without the Activity Execution completing.

There are three steps to follow:

  1. The Activity provides the external system with identifying information needed to complete the Activity Execution. Identifying information can be a Task Token, or a combination of Namespace, Workflow Id, and Activity Id.
  2. The Activity Function completes in a way that identifies it as waiting to be completed by an external system.
  3. The Temporal Client is used to Heartbeat and complete the Activity.

Sometimes Workflows need to perform certain operations in parallel.

Invoking activity stub without the use of yield will return the Activity result promise which can be resolved at later moment. Calling yield on promise blocks until a result is available.

Activity promise also exposes then method to construct promise chains. Read more about Promises here.

Alternatively you can explicitly wrap your code (including yield constucts) using Workflow::async which will execute nested code in parallel with main Workflow code. Call yeild on Promise returned by Workflow::async to merge execution result back to primary Workflow method.

public function greet(string $name): \Generator
{
// Workflow::async runs it's activities and child workflows in a separate coroutine. Use keyword yield to merge
// it back to parent process.

$first = Workflow::async(
function () use ($name) {
$hello = yield $this->greetingActivity->composeGreeting('Hello', $name);
$bye = yield $this->greetingActivity->composeGreeting('Bye', $name);

return $hello . '; ' . $bye;
}
);

$second = Workflow::async(
function () use ($name) {
$hello = yield $this->greetingActivity->composeGreeting('Hola', $name);
$bye = yield $this->greetingActivity->composeGreeting('Chao', $name);

return $hello . '; ' . $bye;
}
);

// blocks until $first and $second complete
return (yield $first) . "\n" . (yield $second);
}

Async completion

There are certain scenarios when moving on from an Activity upon completion of its function is not possible or desirable. For example, you might have an application that requires user input to complete the Activity. You could implement the Activity with a polling mechanism, but a simpler and less resource-intensive implementation is to asynchronously complete a Temporal Activity.

There are two parts to implementing an asynchronously completed Activity:

  1. The Activity provides the information necessary for completion from an external system and notifies the Temporal service that it is waiting for that outside callback.
  2. The external service calls the Temporal service to complete the Activity.

The following example demonstrates the first part:

app/src/AsyncActivityCompletion/GreetingActivity.php

class GreetingActivity implements GreetingActivityInterface
{
private LoggerInterface $logger;

public function __construct()
{
$this->logger = new Logger();
}
/**
* Demonstrates how to implement an Activity asynchronously.
* When {@link Activity::doNotCompleteOnReturn()} is called,
* the Activity implementation function that returns doesn't complete the Activity.
*/
public function composeGreeting(string $greeting, string $name): string
{
// In real life this request can be executed anywhere. By a separate service for example.
$this->logger->info(sprintf('GreetingActivity token: %s', base64_encode(Activity::getInfo()->taskToken)));
// Send the taskToken to the external service that will complete the Activity.
// Return from the Activity a function indicating that Temporal should wait
// for an async completion message.
Activity::doNotCompleteOnReturn();
// When doNotCompleteOnReturn() is invoked the return value is ignored.
return 'ignored';
}
}

The following code demonstrates how to complete the Activity successfully using WorkflowClient:

app/src/AsyncActivityCompletion/CompleteCommand.php

$client = $this->workflowClient->newActivityCompletionClient();
// Complete the Activity.
$client->completeByToken(
base64_decode($input->getArgument('token')),
$input->getArgument('message')
);

To fail the Activity, you would do the following:

// Fail the Activity.
$activityClient->completeExceptionallyByToken($taskToken, new \Error("activity failed"));

Cancel an Activity from a Workflow

Canceling an Activity from within a Workflow requires that the Activity Execution sends Heartbeats and sets a Heartbeat Timeout. If the Heartbeat is not invoked, the Activity cannot receive a cancellation request. When any non-immediate Activity is executed, the Activity Execution should send Heartbeats and set a Heartbeat Timeout to ensure that the server knows it is still working.

When an Activity is canceled, an error is raised in the Activity at the next available opportunity. If cleanup logic needs to be performed, it can be done in a finally clause or inside a caught cancel error. However, for the Activity to appear canceled the exception needs to be re-raised.

note

Unlike regular Activities, Local Activities can be canceled if they don't send Heartbeats. Local Activities are handled locally, and all the information needed to handle the cancellation logic is available in the same Worker process.

How to start a Child Workflow Execution

A Child Workflow Execution is a Workflow Execution that is scheduled from within another Workflow using a Child Workflow API.

When using a Child Workflow API, Child Workflow related Events (StartChildWorkflowExecutionInitiated, ChildWorkflowExecutionStarted, ChildWorkflowExecutionCompleted, etc...) are logged in the Workflow Execution Event History.

Always block progress until the ChildWorkflowExecutionStarted Event is logged to the Event History to ensure the Child Workflow Execution has started. After that, Child Workflow Executions may be abandoned using the default Abandon Parent Close Policy set in the Child Workflow Options.

To be sure that the Child Workflow Execution has started, first call the Child Workflow Execution method on the instance of Child Workflow future, which returns a different future.

Then get the value of an object that acts as a proxy for a result that is initially unknown, which is what waits until the Child Workflow Execution has spawned.

Besides Activities, a Workflow can also start other Workflows.

Workflow::executeChildWorkflow and Workflow::newChildWorkflowStub enables the scheduling of other Workflows from within a Workflow's implementation. The parent Workflow has the ability to monitor and impact the lifecycle of the Child Workflow, similar to the way it does for an Activity that it invoked.

// Use one stub per child workflow run
$child = Workflow::newChildWorkflowStub(
ChildWorkflowInterface::class,
ChildWorkflowOptions::new()
// Do not specify WorkflowId if you want Temporal to generate a unique Id
// for the child execution.
->withWorkflowId('BID-SIMPLE-CHILD-WORKFLOW')
->withExecutionStartToCloseTimeout(DateInterval::createFromDateString('30 minutes'))
);

// This is a non blocking call that returns immediately.
// Use yield $child->workflowMethod(name) to call synchronously.
$promise = $child->workflowMethod('value');

// Do something else here.
try{
$value = yield $promise;
} catch(TemporalException $e) {
$logger->error('child workflow failed');
throw $e;
}

Let's take a look at each component of this call.

Before calling $child->workflowMethod(), you must configure ChildWorkflowOptions for the invocation. These options customize various execution timeouts, and are passed into the Workflow stub defined by the Workflow::newChildWorkflowStub. Once stub created you can invoke its Workflow method based on attribute WorkflowMethod.

The method call returns immediately and returns a Promise. This allows you to execute more code without having to wait for the scheduled Workflow to complete.

When you are ready to process the results of the Workflow, call the yield $promise method on the returned promise object.

When a parent Workflow is cancelled by the user, the Child Workflow can be cancelled or abandoned based on a configurable child policy.

You can also skip the stub part of Child Workflow initiation and use Workflow::executeChildWorkflow directly:

// Use one stub per child workflow run
$childResult = yield Workflow::executeChildWorkflow(
'ChildWorkflowName',
['args'],
ChildWorkflowOptions::new()->withWorkflowId('BID-SIMPLE-CHILD-WORKFLOW'),
Type::TYPE_STRING // optional: defines the return type
);

How to set a Parent Close Policy

A Parent Close Policy determines what happens to a Child Workflow Execution if its Parent changes to a Closed status (Completed, Failed, or Timed Out).

The default Parent Close Policy option is set to terminate the Child Workflow Execution.

In PHP, a Parent Close Policy is set via the ChildWorkflowOptions object and withParentClosePolicy() method. The possible values can be obtained from the ParentClosePolicy class.

  • POLICY_TERMINATE
  • POLICY_ABANDON
  • POLICY_REQUEST_CANCEL

Then ChildWorkflowOptions object is used to create a new Child Workflow object:

$child = Workflow::newUntypedChildWorkflowStub(
'child-workflow',
ChildWorkflowOptions::new()
->withParentClosePolicy(ParentClosePolicy::POLICY_ABANDON)
);

yield $child->start();

In the snippet above we:

  1. Create a new untyped Child Workflow stub with Workflow::newUntypedChildWorkflowStub.
  2. Provide ChildWorkflowOptions object with Parent Close Policy set to ParentClosePolicy::POLICY_ABANDON.
  3. Start Child Workflow Execution asynchronously using yield and method start().

We need yield here to ensure that a Child Workflow Execution starts before the parent closes.

How to Continue-As-New

Continue-As-New enables a Workflow Execution to close successfully and create a new Workflow Execution in a single atomic operation if the number of Events in the Event History is becoming too large. The Workflow Execution spawned from the use of Continue-As-New has the same Workflow Id, a new Run Id, and a fresh Event History and is passed all the appropriate parameters.

Workflows that need to rerun periodically could naively be implemented as a big while loop with a sleep where the entire logic of the Workflow is inside the body of the while loop. The problem with this approach is that the history for that Workflow will keep growing to a point where it reaches the maximum size enforced by the service.

ContinueAsNew is the low level construct that enables implementing such Workflows without the risk of failures down the road. The operation atomically completes the current execution and starts a new execution of the Workflow with the same Workflow Id. The new execution will not carry over any history from the old execution.

To trigger this behavior, use Workflow::continueAsNew or Workflow::newContinueAsNewStub method:

#[Workflow\WorkflowMethod]
public function periodic(string $name, int $value = 0)
{
for ($i = 0; $i < 100; $i++) {
// do something
$value++;
}

// maintain $value counter between runs
return Workflow::newContinueAsNewStub(self::class)->periodic($name, $value);
}

What is a Timer?

A Workflow can set a durable timer for a fixed time period. In some SDKs, the function is called sleep(), and in others, it's called timer().

A Workflow can sleep for months. Timers are persisted, so even if your Worker or Temporal Cluster is down when the time period completes, as soon as your Worker and Cluster are back up, the sleep() call will resolve and your code will continue executing.

Sleeping is a resource-light operation: it does not tie up the process, and you can run millions of Timers off a single Worker.

To set a Timer in PHP, use Workflow::timer() and pass the number of seconds you want to wait before continuing.

The following example yields a sleep method for 5 minutes.

yield Workflow::timer(300); // sleep for 5 minutes

You cannot set a Timer invocation inside the await or awaitWithTimeout methods.

How to use Temporal Cron Jobs

A Temporal Cron Job is the series of Workflow Executions that occur when a Cron Schedule is provided in the call to spawn a Workflow Execution.

A Cron Schedule is provided as an option when the call to spawn a Workflow Execution is made.

Set your Cron Schedule with CronSchedule('* * * * *').

The following example sets a Cron Schedule in PHP:

  $workflow = $this->workflowClient->newWorkflowStub(
CronWorkflowInterface::class,
WorkflowOptions::new()
->withWorkflowId(CronWorkflowInterface::WORKFLOW_ID)
->withCronSchedule('* * * * *')
// Execution timeout limits total time. Cron will stop executing after this timeout.
->withWorkflowExecutionTimeout(CarbonInterval::minutes(10))
// Run timeout limits duration of a single workflow invocation.
->withWorkflowRunTimeout(CarbonInterval::minute(1))
);

$output->writeln("Starting <comment>CronWorkflow</comment>... ");

try {
$run = $this->workflowClient->start($workflow, 'Antony');
// ...
}

Setting withCronSchedule turns the Workflow Execution into a Temporal Cron Job. For more information, see the PHP samples for example code or the PHP SDK WorkflowOptions source code.

How to use Side Effects in PHP

Side Effects are used to execute non-deterministic code, such as generating a UUID or a random number, without compromising determinism in the Workflow. This is done by storing the results of the Side Effect into the Workflow Event History.

A Side Effect doesn't re-execute during a Replay. Instead, it returns the recorded result from the Workflow Execution Event History.

Side Effects shouldn't fail. An exception that is thrown from the Side Effect causes failure and retry of the current Workflow Task.

An Activity or a Local Activity can also be used instead of a Side Effect, as its results are also persisted in Workflow Execution History.

note

You shouldn’t modify the Workflow state inside a Side Effect, because they're not re-executed during Replay. Side Effect functions should only return a value, and that value can be used in Workflow code to alter state.

To use a Side Effect in PHP, use the Workflow::sideEffect() function in your Workflow Definition to run non-deterministic code and return a value.

#[Workflow\WorkflowMethod]
public function run()
{
$random = yield Workflow::sideEffect(fn()=> return random_int(0, 100));
if ($random < 50) {
// ...
} else {
// ...
}
}

How to use Start Delay

Use the Workflow Start Delay functionality if you need to delay the execution of the Workflow without the need for regular launches. Here you simply specify the time to wait before dispatching the first Workflow task.

$workflow = $workflowClient->newWorkflowStub(
GreeterWorkflowInterface::class,
WorkflowOptions::new()
->withWorkflowStartDelay(CarbonInterval::minutes(10)),
);
$workflowClient->start($workflow, 'Hello world!');