Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions SKILL.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
---
name: temporal-developer
description: This skill should be used when the user asks to "create a Temporal workflow", "write a Temporal activity", "debug stuck workflow", "fix non-determinism error", "Temporal Python", "Temporal TypeScript", "workflow replay", "activity timeout", "signal workflow", "query workflow", "worker not starting", "activity keeps retrying", "Temporal heartbeat", "continue-as-new", "child workflow", "saga pattern", "workflow versioning", "durable execution", "reliable distributed systems", or mentions Temporal SDK development.
description: This skill should be used when the user asks to "create a Temporal workflow", "write a Temporal activity", "debug stuck workflow", "fix non-determinism error", "Temporal Python", "Temporal TypeScript", "Temporal PHP", "workflow replay", "activity timeout", "signal workflow", "query workflow", "worker not starting", "activity keeps retrying", "Temporal heartbeat", "continue-as-new", "child workflow", "saga pattern", "workflow versioning", "durable execution", "reliable distributed systems", or mentions Temporal SDK development.
version: 1.0.0
---

# Skill: temporal-developer

## Overview

Temporal is a durable execution platform that makes workflows survive failures automatically. This skill provides guidance for building Temporal applications in Python and TypeScript.
Temporal is a durable execution platform that makes workflows survive failures automatically. This skill provides guidance for building Temporal applications in Python, TypeScript, and PHP.

## Core Architecture

Expand Down Expand Up @@ -92,6 +92,7 @@ Once you've downloaded the file, extract the downloaded archive and add the temp
1. First, read the getting started guide for the language you are working in:
- Python -> read `references/python/python.md`
- TypeScript -> read `references/typescript/typescript.md`
- PHP -> read `references/php/php.md`
2. Second, read appropriate `core` and language-specific references for the task at hand.


Expand Down
1 change: 1 addition & 0 deletions references/core/determinism.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ Each Temporal SDK language provides a protection mechanism to make it easier to

- Python: The Python SDK runs workflows in a sandbox that intercepts and aborts non-deterministic calls at runtime.
- TypeScript: The TypeScript SDK runs workflows in an isolated V8 sandbox, intercepting many common sources of non-determinism and replacing them automatically with deterministic variants.
- PHP: The PHP SDK performs runtime checks that detect adding, removing, or reordering of commands (activity calls, timers, child workflows, etc.). It does NOT have a sandbox — developers must be disciplined about avoiding non-deterministic operations in workflow code.


## Detecting Non-Determinism
Expand Down
111 changes: 111 additions & 0 deletions references/php/advanced-features.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# PHP SDK Advanced Features

## Schedules

Create recurring workflow executions.

```php
use Temporal\Client\Schedule\Schedule;
use Temporal\Client\Schedule\Action\StartWorkflowAction;
use Temporal\Client\Schedule\Spec\ScheduleSpec;
use Temporal\Client\Schedule\Spec\ScheduleIntervalSpec;

$handle = $scheduleClient->createSchedule(
Schedule::new()
->withAction(StartWorkflowAction::new('DailyReportWorkflow')
->withTaskQueue('reports')
)
->withSpec(ScheduleSpec::new()
->withIntervals(new ScheduleIntervalSpec(every: new \DateInterval('P1D')))
),
scheduleId: 'daily-report',
);

// Manage schedules
$handle->pause();
$handle->unpause();
$handle->trigger(); // Run immediately
$handle->delete();
```

## Async Activity Completion

For activities that complete asynchronously (e.g., human tasks, external callbacks).

```php
use Temporal\Activity;

#[ActivityMethod]
public function requestApproval(string $requestId): void
{
// Get task token for async completion
$taskToken = Activity::getInfo()->taskToken;

// Store task token for later completion (e.g., in database)
$this->storeTaskToken($requestId, $taskToken);

// Mark this activity as waiting for external completion
Activity::doNotCompleteOnReturn();
}
```

Complete the activity from another process:

```php
use Temporal\Client\WorkflowClient;

$client = WorkflowClient::create();
$taskToken = getStoredTaskToken($requestId);

$completionClient = $client->newActivityCompletionClient();
$completionClient->complete($taskToken, 'approved');

// Or fail it:
// $completionClient->completeExceptionally($taskToken, new \Exception('Rejected'));
```

**Note:** If the external system can reliably signal back with the result and doesn't need to heartbeat or receive cancellation, consider using **signals** instead.

## Worker Tuning

Configure worker performance settings.

```php
use Temporal\Worker\WorkerOptions;

$worker = $factory->newWorker(
taskQueue: 'my-queue',
options: WorkerOptions::new()
->withMaxConcurrentWorkflowTaskPollers(5)
->withMaxConcurrentActivityTaskPollers(5)
->withMaxConcurrentWorkflowTaskExecutionSize(100)
->withMaxConcurrentActivityExecutionSize(100)
);
```

PHP workers run as RoadRunner processes — the number of concurrent activities is also bounded by the number of RoadRunner worker processes configured in `.rr.yaml`.

## RoadRunner Configuration

PHP uses [RoadRunner](https://roadrunner.dev/) as the process supervisor. Configure it in `.rr.yaml`:

```yaml
version: "3"

temporal:
address: "localhost:7233"
namespace: "default"
activities:
num_workers: 10 # Number of PHP processes for activities
max_jobs: 100 # Restart worker after N jobs (prevents memory leaks)
memory_limit: 128MB # Restart worker if it exceeds this memory limit

server:
command: "php worker.php"
relay: "pipes"
```

Key settings:
- `num_workers` — controls activity concurrency (set based on available CPU/memory)
- `max_jobs` — prevents memory leaks by recycling PHP processes after N executions
- `memory_limit` — safety net for runaway memory usage
232 changes: 232 additions & 0 deletions references/php/data-handling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
# PHP SDK Data Handling

## Overview

The PHP SDK uses data converters to serialize/deserialize Workflow inputs, outputs, and Activity parameters. JSON is the default format.

## Default Data Converter

The default converter handles:
- `null`
- Scalars (`string`, `int`, `float`, `bool`)
- Arrays (JSON-serialized)
- Objects (JSON-serialized via public properties)

**PHP-specific:** Workflow methods are generators. To specify the return type of a Workflow method, use the `#[ReturnType]` attribute on the interface method:

```php
use Temporal\Workflow\ReturnType;

#[WorkflowInterface]
interface OrderWorkflowInterface
{
#[WorkflowMethod]
#[ReturnType(OrderResult::class)]
public function run(OrderInput $input): \Generator;
}
```

Without `#[ReturnType]`, the SDK cannot deserialize the result into the correct class.

## Custom Data Conversion

Implement a custom `PayloadConverter` to handle types the default converter does not support:

```php
use Temporal\DataConverter\PayloadConverter;
use Temporal\Api\Common\V1\Payload;

class MyCustomConverter implements PayloadConverter
{
public function getEncodingType(): string
{
return 'json/my-custom';
}

public function toPayload($value): ?Payload
{
if (!$value instanceof MyCustomType) {
return null; // Return null to let other converters handle it
}

$payload = new Payload();
$payload->setMetadata(['encoding' => $this->getEncodingType()]);
$payload->setData(json_encode($value->toArray()));
return $payload;
}

public function fromPayload(Payload $payload, \ReflectionType $type)
{
return MyCustomType::fromArray(json_decode($payload->getData(), true));
}
}
```

Register the custom converter when creating the `WorkflowClient`:

```php
use Temporal\DataConverter\DataConverter;
use Temporal\DataConverter\JsonPayloadConverter;
use Temporal\DataConverter\NullPayloadConverter;

$dataConverter = new DataConverter(
new NullPayloadConverter(),
new JsonPayloadConverter(),
new MyCustomConverter(),
);

$client = WorkflowClient::create(
ServiceClient::create('localhost:7233'),
dataConverter: $dataConverter
);
```

## Payload Encryption

Encrypt sensitive Workflow data using a custom `PayloadCodec`:

```php
use Temporal\DataConverter\PayloadCodecInterface;
use Temporal\Api\Common\V1\Payload;

class EncryptionCodec implements PayloadCodecInterface
{
public function __construct(private string $key) {}

public function encode(array $payloads): array
{
return array_map(function (Payload $payload) {
$encrypted = $this->encrypt($payload->serializeToString());
$result = new Payload();
$result->setMetadata(['encoding' => 'binary/encrypted']);
$result->setData($encrypted);
return $result;
}, $payloads);
}

public function decode(array $payloads): array
{
return array_map(function (Payload $payload) {
if (($payload->getMetadata()['encoding'] ?? null) !== 'binary/encrypted') {
return $payload;
}
$decrypted = $this->decrypt($payload->getData());
$result = new Payload();
$result->mergeFromString($decrypted);
return $result;
}, $payloads);
}

private function encrypt(string $data): string { /* ... */ }
private function decrypt(string $data): string { /* ... */ }
}
```

Apply the codec via `DataConverter` on the client:

```php
$dataConverter = DataConverter::createDefault()->withCodec(new EncryptionCodec($encryptionKey));

$client = WorkflowClient::create(
ServiceClient::create('localhost:7233'),
dataConverter: $dataConverter
);
```

## Search Attributes

Custom searchable fields for Workflow visibility.

Define Search Attribute keys and set them at Workflow start:

```php
use Temporal\Common\SearchAttributes\SearchAttributeKey;
use Temporal\Common\TypedSearchAttributes;

$orderIdKey = SearchAttributeKey::forKeyword('OrderId');
$orderStatusKey = SearchAttributeKey::forKeyword('OrderStatus');

$workflow = $client->newWorkflowStub(
OrderWorkflowInterface::class,
WorkflowOptions::new()
->withTaskQueue('orders')
->withTypedSearchAttributes(
TypedSearchAttributes::new()
->withSearchAttribute($orderIdKey, $order->id)
->withSearchAttribute($orderStatusKey, 'pending')
)
);
```

Upsert Search Attributes during Workflow execution:

```php
use Temporal\Workflow;
use Temporal\Common\SearchAttributes\SearchAttributeKey;

class OrderWorkflow implements OrderWorkflowInterface
{
public function run(array $order): \Generator
{
// ... process order ...

Workflow::upsertTypedSearchAttributes(
SearchAttributeKey::forKeyword('OrderStatus')->valueSet('completed')
);

return 'done';
}
}
```

### Querying Workflows by Search Attributes

```php
$executions = $client->listWorkflowExecutions(
'OrderStatus = "processing" OR OrderStatus = "pending"'
);

foreach ($executions as $execution) {
echo "Workflow {$execution->getExecution()->getWorkflowId()} is still processing\n";
}
```

## Workflow Memo

Store arbitrary metadata with Workflows (not searchable).

```php
// Set memo at Workflow start
$workflow = $client->newWorkflowStub(
OrderWorkflowInterface::class,
WorkflowOptions::new()
->withTaskQueue('orders')
->withMemo([
'customer_name' => $order->customerName,
'notes' => 'Priority customer',
])
);
```

Upsert memo during Workflow execution:

```php
class OrderWorkflow implements OrderWorkflowInterface
{
public function run(array $order): \Generator
{
// ... process order ...

Workflow::upsertMemo(['status' => 'fraud-checked']);

return yield $this->activity->processPayment($order);
}
}
```

## Best Practices

1. Use `#[ReturnType]` on Workflow interface methods to enable correct deserialization
2. Keep payloads small — see `references/core/gotchas.md` for limits
3. Encrypt sensitive data with a `PayloadCodec`
4. Use typed Search Attributes for business-level visibility and querying
Loading