Releases: temporalio/sdk-php
v2.16.0
Warning
RoadRunner 2025.1.3+ is required.
Abandoned Child Workflow Cancellation
Added a new feature flag FeatureFlags::$cancelAbandonedChildWorkflows to control the cancellation behavior of abandoned Child Workflows.
Previously, when a parent workflow was canceled, all child workflows would be canceled, including those with ParentClosePolicy::Abandon.
This behavior was incorrect - abandoned child workflows should continue running independently when their parent is canceled.
# worker.php
use Temporal\Worker\FeatureFlags;
// Fixed behavior (does NOT cancel abandoned children) - recommended
FeatureFlags::$cancelAbandonedChildWorkflows = false;
// Default behavior (cancels abandoned children - matches previous SDK versions)
FeatureFlags::$cancelAbandonedChildWorkflows = true;Warning
When setting $cancelAbandonedChildWorkflows = false:
- If you start an abandoned child workflow in the main workflow scope, it may miss the cancellation signal if you await only on the child workflow. Use
Promise::race()with a timer to properly handle cancellation. - If you start an abandoned child workflow in an async scope that is later canceled, the child workflow will not be affected by the scope cancellation.
- You can still cancel abandoned child workflows manually by calling
WorkflowStubInterface::cancel().
New Promises
The PHP SDK now supports React Promise v3.
To make this work correctly in the Workflow Worker environment,
the promises have been forked and improved in the internal/promise package.
The fork addresses critical issues for long-running Workflow Workers:
made rejection handler reusable (a v3 feature),
removed exit(255) calls from rejection handling that would terminate the worker process,
added declare(strict_types=1) throughout, and improved type annotations for better static analysis support.
A key improvement is the @yield annotation added to PromiseInterface,
which enables proper type inference when using promises with generators in Workflows.
This annotation is recognized by IDEs (PHPStorm) and static analysis tools (Psalm), significantly improving DX:
interface SomeActivity {
/**
* @return \React\Promise\PromiseInterface<ResultDto>
*/
public function doSomething(int $value): ResultDto;
}
final class Workflow {
public function handle(): \Generator
{
$activity = \Temporal\Workflow::newActivityStub(SomeActivity::class);
$result = yield $activity->doSomething(42); // IDE and Psalm infer $result as ResultDto
}
}The SDK supports both React Promise v2 and v3 - the version used depends on what you require in your composer.json.
Warning
React Promise v3 includes optimizations that may slightly change promise resolution order compared to v2. This could potentially affect Workflow determinism in edge cases.
If you experience issues after upgrading, lock to React Promise v2 in your composer.json:
{
"require": {
"react/promise": "^2.11"
}
}Destroyable Interface
Workflows can now implement the Destroyable interface from the internal/destroy package to explicitly manage resource cleanup when the Workflow instance is evicted from memory.
This is particularly useful when your Workflow contains circular references between objects that prevent PHP's garbage collector from properly cleaning up memory.
While this is not a common scenario,
having explicit control over resource cleanup is critical for long-running Workers handling many workflow executions.
The SDK automatically calls the destroy() method when a Workflow instance needs to be evicted from memory,
allowing you to break circular references and release resources deterministically.
final class Workflow implements Destroyable
{
/** Collection with cross-linked objects that also implements Destroyable */
private LinkedCollection $collection;
// ...
public function destroy(): void
{
// Must be idempotent - safe to call multiple times
$collection = $this->collection ?? null;
unset($this->collection);
$collection?->destroy();
}
}Enhanced Workflow Info
Added new fields to Workflow::getInfo():
- Access the root Workflow execution from any Workflow in the execution chain, including deeply nested child Workflows.
- Access the Workflow's retry policy directly from the Workflow Context.
$rootExecution = Workflow::getInfo()->rootExecution;
$retryOptions = Workflow::getInfo()->retryOptions;RoadRunner PSR Logger
The RoadRunner ecosystem now includes a new roadrunner/psr-logger package that can be used with Temporal SDK.
By default, the SDK uses \Temporal\Worker\Logger\StderrLogger which outputs messages to STDERR.
RoadRunner captures these messages and logs them at the INFO level.
The new \RoadRunner\PsrLogger\RpcLogger sends logs to RoadRunner via RPC with precise log levels and structured context data.
Get Started:
composer require roadrunner/psr-loggeruse RoadRunner\PsrLogger\RpcLogger;
use Spiral\Goridge\RPC\RPC;
use Temporal\WorkerFactory;
$rpc = RPC::create('tcp://127.0.0.1:6001');
$logger = new RpcLogger($rpc);
$factory = WorkerFactory::create(logger: $logger);
$worker = $factory->newWorker('my-task-queue');New Worker Versioning (experimental)
Worker Versioning enables safe deployment of workflow changes by controlling how Workflows move between different worker versions.
Each worker deployment is identified by a unique Build ID, and workflows can be pinned to specific versions or automatically upgrade to the latest version.
Worker Configuration
Configure versioning when creating a worker:
use Temporal\Worker\WorkerOptions;
use Temporal\Worker\WorkerDeploymentOptions;
use Temporal\Common\Versioning\VersioningBehavior;
$worker = $factory->newWorker(
'my-task-queue',
WorkerOptions::new()
->withDeploymentOptions(
WorkerDeploymentOptions::new()
->withUseVersioning(true)
->withVersion('build-v1.2.3')
->withDefaultVersioningBehavior(VersioningBehavior::Pinned)
)
);Workflow Versioning Behavior
Control versioning behavior per workflow using the #[WorkflowVersioningBehavior] attribute:
use Temporal\Workflow;
use Temporal\Common\Versioning\VersioningBehavior;
#[Workflow\WorkflowInterface]
class MyWorkflow
{
#[Workflow\WorkflowMethod]
#[Workflow\WorkflowVersioningBehavior(VersioningBehavior::Pinned)]
public function handle(): \Generator
{
// Workflow will stay pinned to its original deployment version
yield Workflow::timer(3600);
return 'Done';
}
}Versioning Behaviors:
Pinned: Workflow stays on its original deployment version until completionAutoUpgrade: Workflow automatically moves to the current deployment version on the next workflow task
Client Override
Override versioning behavior when starting a workflow:
use Temporal\Client\WorkflowOptions;
use Temporal\Common\Versioning\VersioningOverride;
use Temporal\Common\Versioning\WorkerDeploymentVersion;
// Pin to specific version
$workflow = $client->newWorkflowStub(
MyWorkflow::class,
WorkflowOptions::new()
->withVersioningOverride(
VersioningOverride::pinned(
WorkerDeploymentVersion::fromString('build-v1.2.3')
)
)
);
// Or enable auto-upgrade
$workflow = $client->newWorkflowStub(
MyWorkflow::class,
WorkflowOptions::new()
->withVersioningOverride(VersioningOverride::autoUpgrade())
);Note
This feature is experimental and requires RoadRunner 2025.1.3+.
See the Worker Versioning documentation for deployment strategies and best practices.
Priority Fairness (experimental)
Priority Fairness extends the Task Queue Priority feature with fairness keys and weights,
enabling balanced task processing across multiple tenants or execution groups within a single task queue.
This is particularly valuable for multi-tenant SaaS applications where you need to prevent large tenants from monopolizing worker resources.
Key Concepts:
- Fairness Key: A short string (up to 64 bytes) that groups tasks together, typically representing a tenant ID or priority band (e.g., "premium", "standard", "free")
- Fairness Weight: A float value (0.001 to 1000) that controls the relative processing share for each fairness key. Higher weights receive proportionally more throughput
The fairness mechanism ensures tasks are dispatched in proportion to their weights. For example, with 1000 tenants each having a weight of 1.0, each tenant receives roughly equal task processing throughput regardless of their individual workload size.
Setting Fairness Parameters:
use Temporal\Common\Priority;
use Temporal\Client\WorkflowOptions;
// Start workflow with fairness settings
$workflow = $workflowClient->newWorkflowStub(
OrderWorkflow::class,
WorkflowOptions::new()
->withTaskQueue('task-queue')
->withPriority(
Priority::new()
->withFairnessKey('tenant-123')
->withFairnessWeight(2.5)
),
);In Workflow Context:
use Temporal\Workflow;
use Temporal\Common\Priority;
// Set f...v2.15.1
v2.15.0
Warning
RoadRunner 2025.1.2 is required.
Task Queue Priority
Task Queue Priority allows you to control the execution order of workflows, activities, and child workflows based on assigned priority values within a single task queue. You can select a priority level in the integer range 1...5. A lower value implies higher priority. The default priority if unspecified is in the middle of the range, 3.
Note
As this feature is currently in Pre-release stage, it is not intended for production use at this time.
See product release stages for more information.
Pre-requisites
- If using Temporal Cloud, please contact Temporal support or your Temporal account team to enable this feature for your cloud namespace(s).
- If self-hosting Temporal, use the latest pre-release development server and set
matching.useNewMatcherdynamic config on the relevant task queues (or namespaces).
Client API
# New Priority DTO
$priority = Priority::new(priorityKey: 1);
# Set Priority on a Workflow
$workflow = $workflowClient->newWorkflowStub(
OrderWorkflowInterface::class,
WorkflowOptions::new()
->withTaskQueue('task-queue')
->withPriority($priority),
);Workflow API
# New Priority DTO
$priority = Priority::new(priorityKey: 1);
# Set Priority on an Activity
$activity = Workflow::newActivityStub(
ActivityInterface::class,
ActivityOptions::new()
->withTaskQueue('task-queue')
->withStartToCloseTimeout('5 minutes')
->withPriority($priority),
);
# Set Priority on a Child Workflow
$childWorkflow = Workflow::newChildWorkflowStub(
ChildWorkflowInterface::class,
ChildWorkflowOptions::new()
->withTaskQueue('task-queue')
->withPriority($priority),
);Get Priority value in Workflow or Activity
// Get
$priority = Activity::getInfo()->priority;
$priority = Workflow::getInfo()->priority;Note
- Lower numbers = higher priority.
- Tasks with the same priority are scheduled in FIFO order.
- If priority is unsupported by the server, these settings are silently ignored.
- Remember this feature is not production ready at this stage.
User Metadata
Handler Descriptions
You can now add descriptions to Query, Signal, and Update handlers. Descriptions are available through the description parameter in QueryMethod, SignalMethod, and UpdateMethod attributes, as well as in the Workflow::registerSignal(), Workflow::registerQuery(), and Workflow::registerUpdate() methods. These descriptions will be displayed in the Temporal UI for better handler documentation.
Using Attributes:
#[QueryMethod('get_counter', description: 'Get the current counter value')]
public function getCounter(): int
{
return $this->counter;
}
#[SignalMethod('inc_counter', description: 'Increment the counter value')]
public function incCounter(): void
{
++$this->counter;
}Using Registration Methods:
Workflow::registerQuery('get_counter', $this->getCounter(...), 'Get the current counter value');
Workflow::registerSignal('increment_counter', $this->incrementCounter(...), 'Increment the counter value');Activity and Timer Summaries
You can now add custom metadata summaries to Activity and Timer executions. These summaries will be displayed in the Workflow history within the Temporal UI, providing better visibility into workflow execution details.
Activity Summary:
yield Workflow::executeActivity(
type: 'activity_type',
options: ActivityOptions::new()
->withScheduleToCloseTimeout(30)
->withSummary('Process user payment'),
);Timer Summary:
yield Workflow::timer(
interval: 30,
options: TimerOptions::new()->withSummary('Wait for external service response'),
);Activity Pause
When a heartbeating activity is paused, an ActivityPausedException will be thrown.
Added Activity::getCancellationDetails() that returns ActivityCancellationDetails DTO that provides the reasons for the activity's cancellation.
Pull Requests
- Null coalesce in AttributeMapper instead of try/catch by @shanginn in #615
- Fixed type WorkflowClientCallsInterceptor by @root-aza in #612
- Add versioning acceptance tests by @roxblnfk in #614
- Add Child Workflow ID test by @roxblnfk in #617
- Expose Priority by @roxblnfk in #607
- Expose metadata query support by @roxblnfk in #616
- Add PromiseInterface stub for IDE by @roxblnfk in #627
- Expose
summaryoption for timers and activities by @roxblnfk in #626 - Testing: fixed downloading of
temporal-test-serverfor arm64 by @root-aza in #629 - Fix prototype existence check in ArrayRepository by @roxblnfk in #631
- Process special Temporal builtin prefixes by @roxblnfk in #630
- Activity pause by @roxblnfk in #620
Full Changelog: v2.14.1...v2.15.0
v2.14.1
v2.14.0
Warning
RoadRunner 2024.3.3+ is required.
Workflow Logger
Logging is a critical component for monitoring and troubleshooting your Temporal applications. The PHP SDK now provides a dedicated logger for use within Workflows that respects replay semantics and adds contextual information automatically.
To get a PSR-3 compatible logger in your Workflow code, use the Workflow::getLogger() method:
use Temporal\Workflow;
#[Workflow\WorkflowInterface]
class MyWorkflow
{
#[Workflow\WorkflowMethod]
public function execute(string $param): \Generator
{
Workflow::getLogger()->info('Workflow started', ['parameter' => $param]);
// Your workflow implementation
Workflow::getLogger()->info('Workflow completed');
return 'Done';
}
}Replay Mode Behavior
An important feature of the Workflow logger is its replay-aware behavior. By default, logs are only emitted during the initial Workflow execution and are suppressed during replay to prevent duplicate log entries.
If you want to enable logging during replay (for debugging purposes), you can configure this with the enableLoggingInReplay option:
$factory = WorkerFactory::create();
$worker = $factory->newWorker('your-task-queue', WorkerOptions::new()
->withEnableLoggingInReplay(true)
);Automatic Context Enrichment
The Workflow logger automatically enriches log entries with the current task queue information. Every log message will include a task_queue key in its context, making it easier to filter and correlate logs.
For example, if a log statement is:
$logger->info('Processing order', ['order_id' => 123]);The actual logged context will be:
{ "task_queue": "your-task-queue", "order_id": 123 }
This happens automatically without any additional configuration.
Default Logger
By default, the PHP SDK uses a StderrLogger that outputs log messages to the standard error stream.
These messages are automatically captured by RoadRunner and incorporated into its logging system with the INFO level, ensuring proper log collection in both development and production environments.
For more details on RoadRunner's logging capabilities, see the RoadRunner Logger documentation.
Using a Custom Logger
You can configure your Temporal worker to use a custom PSR-3 compatible logger implementation:
$myLogger = new MyLogger();
$workerFactory = WorkerFactory::create(converter: $converter);
$worker = $workerFactory->newWorker(
taskQueue: 'my-task-queue',
logger: $myLogger,
);Your custom logger will be used throughout the Temporal SDK, including for Workflow logging when accessed through Workflow::getLogger().
getInstance() in Context
Added Activity::getInstance() and Workflow::getInstance() methods to get the current Activity and Workflow instances.
Changed workflow execution flow:
- First, the Workflow is initialized. The
__construct()method is called.- If the
#[WorkflowInit]attribute is present, the handler's arguments are resolved and passed to the constructor. - There you can't make calls to start Activity, ChildWorkflow, Timer, etc.
- If the
WorkflowInboundCallInterceptor::execute()is called- Arguments from the previous step are used, but they can be overridden for the handler call.
- You can call Activity, ChildWorkflow, Timer, etc.
Workflow::getInstance()returns the initialized Workflow instance.- Now errors from this step are recorded in the Workflow history.
- Workflow Handler is called.
Dynamic Handlers
Added methods to define dynamic handlers for Signals, Updates, and Queries that will be called if a handler for a specific name is not found.
// Dynamic Query Handler
\Temporal\Workflow::registerDynamicQuery(function (string $name, ValuesInterface $arguments): string {
return \sprintf(
'Got query `%s` with %d arguments',
$name,
$arguments->count(),
);
});
// Dynamic Update Handler
\Temporal\Workflow::registerDynamicUpdate(
static fn(string $name, ValuesInterface $arguments): string => \sprintf(
'Got update `%s` with %d arguments',
$name,
$arguments->count(),
),
static fn(string $name, ValuesInterface $arguments) => \str_starts_with(
$name,
'update_',
) or throw new \InvalidArgumentException('Invalid update name'),
);User Metadata Support in Client API
Added support for user metadata in Workflow Start/Schedule methods, improving the ability to attach additional information to workflow executions.
- Added ExecutionConfig with UserMetadata in Workflow Description
- Added support for user metadata in Workflow Start/Schedule methods. Metadata in Timers and Activities require changes in RoadRunner and can be added in the future.
Client API
use Temporal\Client\GRPC\ServiceClient;
use Temporal\Client\ScheduleClient;
use Temporal\Client\Schedule\Action\StartWorkflowAction;
use Temporal\Client\WorkflowClient;
use Temporal\Client\WorkflowOptions;
$serviceClient = ServiceClient::create('127.0.0.1:7233');
// Start Workflow with user metadata
$workflowClient = WorkflowClient::create($serviceClient);
$stub = $workflowClient->newUntypedWorkflowStub(
'SimpleWorkflow',
(new WorkflowOptions())
->withStaticSummary('some text')
->withStaticDetails('details')
);
$workflowClient->start($stub);
// Describe workflow
echo $stub->describe()->config->userMetadata->summary;
echo $stub->describe()->config->userMetadata->details;
// Schedule Workflow with user metadata
$scheduleClient = ScheduleClient::create($serviceClient);
$schedule = $scheduleClient->createSchedule(
\Temporal\Client\Schedule\Schedule::new()
->withAction(StartWorkflowAction::new(SimpleWorkflow::class)
->withStaticSummary('some-summary')
->withStaticDetails('some-details'))
);
// Describe schedule
$action = $schedule->describe()->schedule->action;
assert($action instanceof StartWorkflowAction);
echo $action->userMetadata->details;
echo $action->userMetadata->summary;Workflow context:
$stub = \Temporal\Workflow::newChildWorkflowStub(
SimpleWorkflow::class,
(new Workflow\ChildWorkflowOptions())
->withStaticSummary('some text')
->withStaticDetails('details')
);Additional Improvements
- Skip magic methods in Activity classes: Magic methods not marked by the ActivityMethod attribute will not be registered as activity methods
- Enhanced Workflow description info: Added new fields into Workflow stub -> describe result:
- rootExecution
- firstRunId
- executionDuration
Pull requests
- Features:
- Expose Activity and Workflow instance from context by @roxblnfk in #593
- Add methods to register fallback handlers by @roxblnfk in #581
- Add Workflow Logger by @roxblnfk in #589
- Skip magic methods in Activity classes by @roxblnfk in #572
- Update Workflow description info by @roxblnfk in #578
- Support UserMetadata in Client API by @roxblnfk in #585
- SDK Hotfixes:
- Maintenance
- Maintenance by @roxblnfk in #606
- Refresh and simplify CI Workflows by @roxblnfk in #600
- Add CODEOWNERS by @Sushisource in #604
- Fix vercel publishing by @Sushisource in #609
New Contributors
- @Sushisource made their first contribution in #604
- @DanielsStugis made their first contribution in #594
Full Changelog: v2.13.0...v2.14.0
v2.13.4
v2.13.3
v2.13.2
v2.13.1
v2.13.0
Warning
RoadRunner 2024.3.3 is required.
Search Attributes
A new approach for working with Search Attributes has been implemented - Typed Search Attributes.
For this, new methods have been added to WorkflowOptions DTO and Workflow facade.
$keyDestinationTime = SearchAttributeKey::forDatetime('DestinationTime');
$keyOrderId = SearchAttributeKey::forKeyword('OrderId');
$workflow = $workflowClient->newWorkflowStub(
OrderWorkflowInterface::class,
WorkflowOptions::new()
->withWorkflowExecutionTimeout('10 minutes')
->withTypedSearchAttributes(
TypedSearchAttributes::empty()
->withValue($keyOrderId, $orderid)
->withValue($keyDestinationTime, new \DateTimeImmutable('2028-11-05T00:10:07Z'))
),
);
#[Workflow\WorkflowInterface]
class OrderWorkflowInterface {
// ...
#[Workflow\UpdateMethod]
public function postponeDestinationTime(\DateInterval $interval)
{
// Get keys to work with
$keyDestinationTime = SearchAttributeKey::forDatetime('DestinationTime');
$keyToRemove = SearchAttributeKey::forKeyword('SomeFieldToRemove');
/** @var DateTimeImmutable $destinationTime */
$destinationTime = Workflow::getInfo()->typedSearchAttributes->get($keyDestinationTime);
Workflow::upsertTypedSearchAttributes(
$keyDestinationTime->valueSet($destinationTime->add($interval)),
$keyToRemove->valueUnset(),
);
}
}When starting the Temporal Dev Server, you can specify types for Search Attributes.
$testEnv->startTemporalServer(searchAttributes: [
'testBool' => ValueType::Bool,
'testInt' => ValueType::Int,
'testFloat' => ValueType::Float,
'testString' => ValueType::String,
'testKeyword' => ValueType::Keyword,
'testKeywordList' => ValueType::KeywordList,
'testDatetime' => ValueType::Datetime,
]);Workflow Init
The new #[WorkflowInit] attribute has been added for the Workflow class constructor
This attribute allows you to receive arguments in the constructor that were passed when the Workflow was started.
The Workflow input arguments are also passed to your #[WorkflowMethod] method -- that always happens, whether or not you use the #[WorkflowInit] attribute.
This is useful if you have message handlers that need access to Workflow input: see Initializing the Workflow first.
use Temporal\Workflow;
#[Workflow\WorkflowInterface]
class GreetingExample
{
private readonly string $nameWithTitle;
private bool $titleHasBeenChecked;
// Note the attribute is on a public constructor
#[Workflow\WorkflowInit]
public function __construct(string $input)
{
$this->nameWithTitle = 'Sir ' . $input;
$this->titleHasBeenChecked = false;
}
#[Workflow\WorkflowMethod]
public function getGreeting(string $input)
{
yield Workflow::await(fn() => $this->titleHasBeenChecked);
return "Hello " . $this->nameWithTitle;
}
#[Workflow\UpdateMethod]
public function checkTitleValidity()
{
// 👉 The handler is now guaranteed to see the workflow input
// after it has been processed by the constructor.
$isValid = yield Workflow::executeActivity('activity.checkTitleValidity', [$this->nameWithTitle]);
$this->titleHasBeenChecked = true;
return $isValid;
}
}Warning
By default, the Workflow Handler runs before Signals and Updates in PHP SDK v2. This behavior is incorrect.
To avoid breaking already written Workflows, since PHP SDK v2.11.0, a feature flag was added to enhance the behavior of the Workflow Handler.
Make sure to set this flag to true to enable the correct behavior.
Memo
Added a method to update the Workflow's Memo Workflow::upsertMemo.
Workflow::upsertMemo([
'key1' => 'foo',
'key2' => 42,
'key3' => ['subkey1' => 'value']
'key4' => null, // remove key4
});MetaStorm metadata
To improve Developer Experience, metadata for the MetaStorm plugin has been added.
If you use MetaStorm, the IDE will now suggest Workflow classes and types in the corresponding methods.
Pull Requests
- Expose Typed Search Attributes by @roxblnfk in #553
- Expose Workflow Init Method by @roxblnfk in #556
- Configuring Typed Search Attributes when starting the DEV server. by @roxblnfk in #560
- Add metadata for Meta Storm plugin by @roxblnfk in #543
- Search Attributes refactoring by @roxblnfk in #561
- Fix unmarshalling of ScheduleSpec with null jitter by @roxblnfk in #563
- Expose
Workflow::upsertMemo()by @roxblnfk in #562 - Rename
InitMethodtoWorkflowInitby @roxblnfk in #564 - Rename
SearchAttributeKey::forStringtoSearchAttributeKey::forTextby @roxblnfk in #565
Full Changelog: v2.12.3...v2.13.0
