NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
Packagist · #1328 most downloaded on Packagist
Temporal SDK
Last release 20 days ago
14 Sep 2026
Release timing varies
gaps range from 2 weeks to 6 months
Rarely documented
notes for 12 of the last 60 stable releases
Nothing withdrawn
no release was ever pulled
6 years old
93 releases · first in 2021
One column per quarter.
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
A small release: a client for the Temporal Cloud Operations API, an opt-in change to multi-condition await , and a set of correctness, including compa
A small release: a client for the Temporal Cloud Operations API, an opt-in change to multi-condition await, and a set of correctness, including compatibility fixes for ext-protobuf 3.x and performance improvements.
A CloudClient / CloudClientInterface pair exposes the Cloud Operations API
through the SDK's own gRPC abstraction, so it shares connection handling, metadata, interceptors,
retries and call context with WorkflowClient and OperatorClient.
withApiVersion() pins the required temporal-cloud-api-version header on every call; a version
already present on the call context wins.
use Temporal\Api\Cloud\Cloudservice\V1\GetNamespacesRequest;
use Temporal\Client\GRPC\CloudClient;
$cloud = CloudClient::createSSL('saas-api.tmprl.cloud:443')
->withAuthKey($apiKey)
->withApiVersion('2024-05-13-00');
$response = $cloud->GetNamespaces(new GetNamespacesRequest());The stubs come from the CloudService gRPC definitions shipped in roadrunner-api-dto 1.15.0,
so the package constraint moves to ^1.15.0.
Closes #476.
await() on the First Settled ConditionA rejected promise passed to a multi-condition Workflow::await() / awaitWithTimeout() used to be
swallowed until the timeout fired. With the new flag the await unblocks on the first settled
condition and propagates the rejection (Promise::any → Promise::race); the pending internal
timer of awaitWithTimeout() is cancelled on that path too.
Because this changes the commands a workflow emits, it is gated behind a feature flag — default off
keeps existing histories replay-compatible, mirroring sdk-java's CANCEL_AWAIT_TIMER_ON_CONDITION.
\Temporal\Worker\FeatureFlags::$settleAwaitOnFirstSettledCondition = true;
// $activityPromise rejects -> the await rejects immediately instead of waiting for the timeout
yield Workflow::awaitWithTimeout('10 minutes', $activityPromise, fn(): bool => $this->done);To wait for the first fulfilled condition and keep ignoring rejections, combine the promises
explicitly: yield Workflow::await(Promise::any([$a, $b])).
Closes #399.
This release focuses on extensibility, broader client coverage, and a stronger testing story, along with a set of correctness fixes and dependency upd
This release focuses on extensibility, broader client coverage, and a stronger testing story, along with a set of correctness fixes and dependency updates.
A new plugin architecture lets you hook into the SDK lifecycle — connection, client, schedule client, worker and worker factory. Each stage has its own interface with a no-op trait, so a plugin implements only the methods it cares about by extending AbstractPlugin.
use Temporal\Plugin\AbstractPlugin;
use Temporal\Plugin\WorkerPluginContext;
final class MetricsPlugin extends AbstractPlugin
{
public function __construct()
{
parent::__construct(name: 'metrics');
}
// override only the hooks you need; the rest are no-ops.
// each hook is middleware — call $next to continue the chain.
public function configureWorker(WorkerPluginContext $context, callable $next): void
{
$context->addInterceptor(new MyInterceptor());
$next($context);
}
}Plugins are registered by name in a PluginRegistry (duplicate names raise a \RuntimeException); WorkflowClient::create() accepts a registry and worker plugins registered on the client are auto-propagated to workers.
A repo-canonical OperatorClient / OperatorClientInterface exposes Temporal operator RPCs — including DeleteNamespace — through the SDK's own client abstraction. The shared gRPC plumbing (connection handling, metadata, interceptors, retries, call context) was generalized so workflow and operator clients behave identically.
use Temporal\Api\Operatorservice\V1\DeleteNamespaceRequest;
use Temporal\Client\GRPC\OperatorClient;
$operatorClient = OperatorClient::create('127.0.0.1:7233');
$response = $operatorClient->DeleteNamespace(
(new DeleteNamespaceRequest())->setNamespace('example-namespace'),
);Activity::heartbeat() now throws a new ActivityResetException when the server reports the attempt was reset (alongside cancelled/paused), and the reset state is exposed via ActivityCancellationDetails::$reset.
use Temporal\Activity;
use Temporal\Exception\Client\ActivityResetException;
try {
Activity::heartbeat($progress);
} catch (ActivityResetException) {
// attempt was reset by the user
}
// or inspect without throwing:
if (Activity::getCancellationDetails()?->reset) {
// ...
}Workflow::sideEffect() now accepts an optional SideEffectOptions carrying a single-line summary (surfaced in the Web UI / CLI).
use Temporal\Common\SideEffectOptions;
$id = yield Workflow::sideEffect(
fn() => Uuid::v4(),
SideEffectOptions::new()->withSummary('generate correlation id'),
);The
summaryfield is currently marked experimental.
Local activities gain the same summary-metadata support as regular activities via LocalActivityOptions::withSummary().
use Temporal\Activity\LocalActivityOptions;
$stub = Workflow::newActivityStub(
Billing::class,
LocalActivityOptions::new()
->withStartToCloseTimeout(5)
->withSummary('charge customer'),
);On the updateWithStart caller path, the resultType and firstExecutionRunId from UpdateOptions are now propagated to the server request (previously hardcoded), so the update result can be typed instead of stdClass.
use Temporal\Client\Update\UpdateOptions;
use Temporal\Client\Update\LifecycleStage;
$options = UpdateOptions::new('echo', LifecycleStage::StageCompleted)
->withResultType(UpdateResult::class);
$handle = $client->updateWithStart($stub, $options, ['hello']);
$result = $handle->getResult(); // instance of UpdateResultA dedicated set of changes brings the PHP testing framework close to Go/Java/TS: outbound calls can be stubbed, time-driven workflows run under time-skipping, and history can be asserted after a run.
Child workflows were the last outbound call with no way to stub them, and activity mocks couldn't vary the result per call.
// Stub a child workflow — no real child execution is started
$this->workflowMocks->expectCompletion('GreetChild', 'Hello, Antony');
$this->workflowMocks->expectFailure('GreetChild', new \RuntimeException('boom'));
$this->workflowMocks->expectCompletionWhen('GreetChild', ['Antony'], 'Hi');
// Per-call activity results inside a loop
$this->activityMocks->expectConsecutiveCompletions('Order.charge', [true, false, true]);
// Arg-matched activity result
$this->activityMocks->expectCompletionWhen('Order.charge', [100], true);
// getVersion / sideEffect mocks
$this->workflowMocks->expectVersion('migrate-db', 2);
$this->workflowMocks->expectSideEffect('deterministic-value');History-based assertions over a finished run via WorkflowInteractions ($this->interactions($run)):
$this->interactions($run)->activity('Order.charge')->withInput(100)->assertCalledOnce();
$this->interactions($run)->childWorkflow('GreetChild')->assertStartedTimes(3);
$this->interactions($run)->timer()->assertStarted('PT30M');
$this->interactions($run)->signal('approve')->assertNeverSent();
$this->interactions($run)->assertNoOtherActivities();Closes [#524](#524), [#302](#302).
Timer-driven workflows are now testable under time-skipping (composer test:func-timeskip), and a client-side scheduler fires signals/queries/cancels at simulated-time offsets — so a 30-minute-timer workflow completes in seconds of wall-clock.
final class TimerTest extends TimeSkippingWorkflowTestCase
{
public function testDelayedSignals(): void
{
$stub = $this->workflowClient->newUntypedWorkflowStub('SignalCollectorWorkflow');
$this->delayedCallbacks
->signalAfter(60, 'add', 'a')
->signalAfter(120, 'add', 'b')
->start($stub);
$this->assertSame(['a', 'b'], $stub->getResult());
}
}TimeLockingInterceptor + TimeSkippingWorkflowTestCase — timer-only workflows and ActivityMocker now run under time-skipping.WorkflowTestCase::tearDown so a leaked lock no longer poisons the suite.Closes [#743](#743), [#745](#745), [#529](#529), [#744](#744).
Workflows that upsert typed search attributes can now run against the time-skipping test server (which doesn't implement that RPC), and the upserts are assertable.
$this->searchAttributeMocks->assertUpserted('customKeywordField');
$this->searchAttributeMocks->assertUpsertedValue('customIntField', 42);
$this->searchAttributeMocks->assertUnset('obsoleteField');Acceptance bootstrapping moved into a PHPUnit Extension that collects the test classes PHPUnit actually selected and passes them to the RoadRunner worker (test-class= arg), so only the needed task queues are created instead of one per test. A filtered run no longer pays for every feature's queue:
composer test:accept -- --filter 'UpdateWorkflowTestCase'A Temporal\Testing\Transcript module records inbound/outbound wire frames, logs, raised errors and workflow-history-on-failure to per-run files while debugging. It ships as a worker plugin (TranscriptPlugin, built on the new plugin system) and adds composer scripts to view/merge/clean output; location is controlled by TEMPORAL_TRANSCRIPT_* env vars.
TEMPORAL_TRANSCRIPT_DUMP_ON_FAIL=1 composer test:accept
composer transcripts:mergeRPC cancellation during update/result waits is now surfaced consistently as a timeout-or-canceled condition, aligned with the Go, Java and TypeScript SDKs.
Cancellation now propagates to scopes and awaits that are registered after a cancel has already fired (closes [#769](#769)). Gated behind a feature flag (default off):
\Temporal\Worker\FeatureFlags::$propagateCancellationToNewScopes = true;When the caller supplies no update ID, the client now generates a UUID v4 (Common\Uuid::v4()) instead of sending an empty string — fixing updates that never resolved on the time-skipping test server, and matching Go/Java/TS.
A method-level #[MethodRetry] is now merged with (rather than overridden by) options passed at call time — options set explicitly at the call site win, and the rest fall back to the attribute.
#[ActivityMethod]
#[MethodRetry(maximumAttempts: 5, initialInterval: '1s')]
public function charge(int $amount): void;
// call-site options merge on top of the attribute:
// maximumAttempts stays 5, initialInterval overridden to 2s
$activity = Workflow::newActivityStub(
Billing::class,
ActivityOptions::new()
->withRetryOptions(RetryOptions::new()->withInitialInterval('2s')),
);TickInfo::applyTo and adopt it in command routers (@xepozz)TemporalStarter dependency causing server reboots (@xepozz)default task queue (@xepozz)spiral/gh-actions cs-fix workflow to commit SHA (@xepozz)Your coding agent can read these notes before it upgrades. Set up the MCP server →