Skip to content

Commit fa2cdd4

Browse files
drewhoskins-temporalchris-olszewskijmaeagle99lennessyyclaude
authored
Improve replay-safety documentation for interceptors, clarify where interceptors run. (#4307)
* Help users discover plugin registration from Interceptor docs * Python tweaks * Better workflow determinism advice for python and typescript * Better workflow determinism advice for ruby, .NET, go, and java * Code review fixups * Clarify where interceptors run. * Tim feedback * Merge conflicts * Update docs/develop/ruby/core-application.mdx Co-authored-by: Chris Olszewski <chrisdolszewski@gmail.com> * Chris feedback * Doc isReplayingHistoryEvents for typescript * Polish based on Maple's friction log * Update docs/develop/dotnet/core-application.mdx Co-authored-by: Justin Anderson <44687433+jmaeagle99@users.noreply.github.com> * Update docs/develop/dotnet/core-application.mdx Co-authored-by: Justin Anderson <44687433+jmaeagle99@users.noreply.github.com> * Update docs/develop/dotnet/core-application.mdx Co-authored-by: Justin Anderson <44687433+jmaeagle99@users.noreply.github.com> * Use caution admonition for replay determinism warning across all SDKs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Chris Olszewski <chrisdolszewski@gmail.com> Co-authored-by: Justin Anderson <44687433+jmaeagle99@users.noreply.github.com> Co-authored-by: Lenny Chen <55669665+lennessyy@users.noreply.github.com> Co-authored-by: Lenny Chen <lenny.chen@temporal.io> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7788392 commit fa2cdd4

9 files changed

Lines changed: 394 additions & 24 deletions

File tree

docs/develop/dotnet/core-application.mdx

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,60 @@ This means there are several things Workflows cannot do such as:
8484
Some calls in .NET do unsuspecting non-deterministic things and are easy to accidentally use.
8585
This is especially true with `Task`s.
8686
Temporal requires that the deterministic `TaskScheduler.Current` is used, but many .NET async calls will use `TaskScheduler.Default` implicitly (and some analyzers even encourage this).
87+
88+
The following sections cover replay-safe APIs, followed by .NET-specific `Task` gotchas.
89+
90+
#### Logging
91+
92+
Use [`Workflow.Logger`](https://dotnet.temporal.io/api/Temporalio.Workflows.Workflow.html#Temporalio_Workflows_Workflow_Logger), which is an instance of .NET's `ILogger`. The SDK logger automatically suppresses log messages during replay to avoid duplicates:
93+
94+
```csharp
95+
Workflow.Logger.LogInformation("Starting workflow for {Name}", name);
96+
```
97+
98+
For logger configuration, see [Observability: Logging](/develop/dotnet/observability#logging).
99+
100+
#### Random numbers and UUIDs
101+
102+
Use [`Workflow.Random`](https://dotnet.temporal.io/api/Temporalio.Workflows.Workflow.html#Temporalio_Workflows_Workflow_Random) to get a deterministic `Random` instance. For UUIDs, use [`Workflow.NewGuid()`](https://dotnet.temporal.io/api/Temporalio.Workflows.Workflow.html#Temporalio_Workflows_Workflow_NewGuid). Never use `System.Random` or `Guid.NewGuid()` directly:
103+
104+
```csharp
105+
// Good - deterministic across replays
106+
var value = Workflow.Random.Next(1, 100);
107+
var uniqueId = Workflow.NewGuid();
108+
109+
// Bad - different result on every replay
110+
var value = new Random().Next(1, 100);
111+
```
112+
113+
#### Current time
114+
115+
Use [`Workflow.UtcNow`](https://dotnet.temporal.io/api/Temporalio.Workflows.Workflow.html#Temporalio_Workflows_Workflow_UtcNow) instead of `DateTime.UtcNow`. The SDK returns the time of the last Workflow Task, which is consistent across replays:
116+
117+
```csharp
118+
var currentTime = Workflow.UtcNow;
119+
```
120+
121+
#### Detecting replay (advanced)
122+
123+
Use [`Workflow.Unsafe.IsReplaying`](https://dotnet.temporal.io/api/Temporalio.Workflows.Workflow.Unsafe.html#Temporalio_Workflows_Workflow_Unsafe_IsReplaying) to guard code that should only run on the first execution, such as emitting metrics or sending external notifications from an Interceptor.
124+
:::caution
125+
126+
Never use this to affect Workflow business logic — branching on replay status breaks determinism.
127+
128+
:::
129+
130+
```csharp
131+
if (!Workflow.Unsafe.IsReplaying)
132+
{
133+
EmitMetric("workflow_started", 1);
134+
}
135+
```
136+
137+
If your goal is to always take action when something new is happening, check that `Workflow.Unsafe.IsReplayingHistoryEvents` is false instead. This will be false during read-only operations like queries and update validators. This is what the SDK's built-in logger and metric meter use internally.
138+
139+
#### .NET Task gotchas
140+
87141
Here are some known gotchas to avoid with .NET tasks inside of Workflows:
88142

89143
- Do not use `Task.Run` - this uses the default scheduler and puts work on the thread pool.

docs/develop/go/core-application.mdx

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,56 @@ The Temporal Go SDK has APIs to handle equivalent Go constructs:
403403
- `workflow.Context` This is a replacement for `context.Context`.
404404
See [Tracing](/develop/go/observability#tracing) for more information about context propagation.
405405

406+
The following sections describe the most common replay-safe patterns.
407+
408+
#### Logging
409+
410+
Use [`workflow.GetLogger(ctx)`](https://pkg.go.dev/go.temporal.io/sdk/workflow#GetLogger) instead of the standard `log` package. The SDK logger automatically suppresses log messages during replay to avoid duplicates:
411+
412+
```go
413+
logger := workflow.GetLogger(ctx)
414+
logger.Info("Starting workflow", "name", name)
415+
```
416+
417+
For logger configuration, see [Observability: Logging](/develop/go/observability#logging).
418+
419+
#### Random numbers and UUIDs
420+
421+
Use [`workflow.SideEffect`](https://pkg.go.dev/go.temporal.io/sdk/workflow#SideEffect) to capture non-deterministic values like random numbers. The result is stored in the Event History and reused on replay instead of re-executing:
422+
423+
```go
424+
encodedRandom := workflow.SideEffect(ctx, func(ctx workflow.Context) interface{} {
425+
return rand.Intn(100)
426+
})
427+
var random int
428+
encodedRandom.Get(&random)
429+
```
430+
431+
For more details, see [Side Effects](/develop/go/side-effects).
432+
433+
#### Current time
434+
435+
Use [`workflow.Now(ctx)`](https://pkg.go.dev/go.temporal.io/sdk/workflow#Now) instead of `time.Now()`. The SDK returns the time of the last Workflow Task, which is consistent across replays:
436+
437+
```go
438+
currentTime := workflow.Now(ctx)
439+
```
440+
441+
#### Detecting replay (advanced)
442+
443+
Use [`workflow.IsReplaying(ctx)`](https://pkg.go.dev/go.temporal.io/sdk/workflow#IsReplaying) to guard code that should only run on the first execution, such as emitting metrics or sending external notifications from an Interceptor.
444+
:::caution
445+
446+
Never use this to affect Workflow business logic — branching on replay status breaks determinism.
447+
448+
:::
449+
450+
```go
451+
if !workflow.IsReplaying(ctx) {
452+
emitMetric("workflow_started", 1)
453+
}
454+
```
455+
406456
## How to develop an Activity Definition in Go {#activity-definition}
407457

408458
In the Temporal Go SDK programming model, an Activity Definition is an exportable function or a `struct` method.

docs/develop/java/core-application.mdx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,59 @@ The following constraints apply when writing Workflow Definitions:
269269
A single implementation can implement a Workflow Type which by definition is dynamically loaded from some external source.
270270
All standard `WorkflowOptions` and determinism rules apply to Dynamic Workflow implementations.
271271

272+
The following sections describe the most common replay-safe patterns.
273+
274+
#### Logging
275+
276+
Use [`Workflow.getLogger()`](https://www.javadoc.io/doc/io.temporal/temporal-sdk/latest/io/temporal/workflow/Workflow.html) instead of SLF4J loggers directly. The SDK logger automatically omits log messages during replay:
277+
278+
```java
279+
private static final Logger logger = Workflow.getLogger(MyWorkflow.class);
280+
281+
// Inside workflow method:
282+
logger.info("Starting workflow for {}", name);
283+
```
284+
285+
For logger configuration, see [Observability: Logging](/develop/java/observability#logging).
286+
287+
#### Random numbers and UUIDs
288+
289+
Use [`Workflow.newRandom()`](https://javadoc.io/doc/io.temporal/temporal-sdk/latest/io/temporal/workflow/Workflow.html#newRandom) for deterministic random numbers, and [`Workflow.randomUUID()`](https://www.javadoc.io/static/io.temporal/temporal-sdk/latest/io/temporal/workflow/Workflow.html#randomUUID()) for deterministic UUIDs. Never use `java.util.Random` or `UUID.randomUUID()` directly:
290+
291+
```java
292+
// Good - deterministic across replays
293+
int randomInt = Workflow.newRandom().nextInt(100);
294+
String uniqueId = Workflow.randomUUID().toString();
295+
296+
// Bad - different result on every replay
297+
int randomInt = new Random().nextInt(100);
298+
```
299+
300+
For other non-deterministic values, use [`Workflow.sideEffect()`](https://www.javadoc.io/doc/io.temporal/temporal-sdk/latest/io/temporal/workflow/Workflow.html#sideEffect(java.lang.Class,io.temporal.workflow.Functions.Func)). See [Side Effects](/develop/java/side-effects) for details.
301+
302+
#### Current time
303+
304+
Use [`Workflow.currentTimeMillis()`](https://www.javadoc.io/doc/io.temporal/temporal-sdk/latest/io/temporal/workflow/Workflow.html) instead of `System.currentTimeMillis()` or `Instant.now()`:
305+
306+
```java
307+
long currentTime = Workflow.currentTimeMillis();
308+
```
309+
310+
#### Detecting replay (advanced)
311+
312+
Use [`WorkflowUnsafe.isReplaying()`](https://www.javadoc.io/doc/io.temporal/temporal-sdk/latest/io/temporal/workflow/WorkflowUnsafe.html) to guard code that should only run on the first execution, such as emitting metrics or sending external notifications from an Interceptor.
313+
:::caution
314+
315+
Never use this to affect Workflow business logic — branching on replay status breaks determinism.
316+
317+
:::
318+
319+
```java
320+
if (!WorkflowUnsafe.isReplaying()) {
321+
emitMetric("workflow_started", 1);
322+
}
323+
```
324+
272325
Java Workflow reference: [https://www.javadoc.io/doc/io.temporal/temporal-sdk/latest/io/temporal/workflow/package-summary.html](https://www.javadoc.io/doc/io.temporal/temporal-sdk/latest/io/temporal/workflow/package-summary.html)
273326

274327
## Develop a basic Activity {#develop-activities}

docs/develop/python/core-application.mdx

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ Workflow logic is constrained by [deterministic execution requirements](/workflo
201201
Therefore, each language is limited to the use of certain idiomatic techniques. However, each Temporal SDK provides a
202202
set of APIs that can be used inside your Workflow to interact with external (to the Workflow) application code.
203203

204-
Workflow code must be deterministic. This means:
204+
Workflow code must be deterministic because the Temporal Server may [replay](/develop/python/testing-suite#replay) your Workflow to reconstruct its state. This means:
205205

206206
- no threading
207207
- no randomness
@@ -214,6 +214,63 @@ All API safe for Workflows used in the [`temporalio.workflow`](https://python.te
214214
run in the implicit [`asyncio` event loop](https://docs.python.org/3/library/asyncio-eventloop.html) and be
215215
_deterministic_.
216216

217+
The SDK provides replay-safe alternatives for common needs:
218+
219+
#### Logging
220+
221+
Use [`workflow.logger`](https://python.temporal.io/temporalio.workflow.html#logger) instead of `print()` or the standard `logging` module.
222+
The SDK logger automatically suppresses log messages during replay to avoid duplicates:
223+
224+
```python
225+
@workflow.defn
226+
class MyWorkflow:
227+
@workflow.run
228+
async def run(self, name: str) -> str:
229+
workflow.logger.info("Starting workflow", name)
230+
# ...
231+
```
232+
233+
For logger configuration, see [Observability: Log from a Workflow](/develop/python/observability#logging).
234+
235+
#### Random numbers and UUIDs
236+
237+
Use [`workflow.random()`](https://python.temporal.io/temporalio.workflow.html#random) to get a deterministic `random.Random` instance seeded per Workflow Execution. Never use `random.random()` or other `random` module functions directly.
238+
For UUIDs, use [`workflow.uuid4()`](https://python.temporal.io/temporalio.workflow.html#uuid4) instead of `uuid.uuid4()`:
239+
240+
```python
241+
# Good - deterministic across replays
242+
value = workflow.random().randint(1, 100)
243+
unique_id = workflow.uuid4()
244+
245+
# Bad - different result on every replay
246+
import random
247+
value = random.randint(1, 100)
248+
```
249+
250+
#### Current time
251+
252+
Use [`workflow.now()`](https://python.temporal.io/temporalio.workflow.html#now) instead of `datetime.now()` or `time.time()`. The SDK returns the time of the last Workflow Task, which is consistent across replays:
253+
254+
```python
255+
current_time = workflow.now()
256+
```
257+
258+
#### Detecting replay (advanced)
259+
260+
Use [`workflow.unsafe.is_replaying`](https://python.temporal.io/temporalio.workflow.html#is_replaying) to guard code that should only run on the first execution, such as emitting metrics or sending external notifications from an [Interceptor](/develop/python/interceptors).
261+
:::caution
262+
263+
Never use this to affect Workflow business logic — branching on replay status breaks determinism.
264+
265+
:::
266+
267+
```python
268+
if not workflow.unsafe.is_replaying():
269+
emit_metric("workflow_started", 1)
270+
```
271+
272+
If your goal is to always take action when something new is happening, check that `workflow.unsafe.is_replaying_history_events()` is false instead. This will be false during read-only operations like queries and update validators. This is what the SDK's built-in logger and tracing interceptors use internally.
273+
217274
## Develop a basic Activity {#develop-activities}
218275

219276
**How to develop a basic Activity using the Temporal Python SDK.**

docs/develop/python/interceptors.mdx

Lines changed: 67 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,71 @@ There are two types of interceptors--inbound and outbound.
2828

2929
Concretely, there are five categories of inbound and outbound calls that you can modify in this way:
3030

31-
| [Outbound Client calls](https://python.temporal.io/temporalio.client.OutboundInterceptor.html) | [Inbound Workflow calls](https://python.temporal.io/temporalio.worker.WorkflowInboundInterceptor.html) | [Outbound Workflow calls](https://python.temporal.io/temporalio.worker.WorkflowOutboundInterceptor.html) | [Inbound Activity calls](https://python.temporal.io/temporalio.worker.ActivityInboundInterceptor.html) | [Outbound Activity calls](https://python.temporal.io/temporalio.worker.ActivityOutboundInterceptor.html) |
32-
| ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------- |
33-
| Start workflow, signal workflow, list workflows, update schedule | Execute workflow (handles a Workflow Task that starts a new Workflow Execution), handle query, handle signal, handle update handler, handle update validator | Start activity, start child workflow, signal child workflow, signal external workflow, start Nexus operation, start local activity | Execute activity (this is the only inbound Activity call) | Info, heartbeat |
31+
| | [Outbound Client](https://python.temporal.io/temporalio.client.OutboundInterceptor.html) | [Inbound Workflow](https://python.temporal.io/temporalio.worker.WorkflowInboundInterceptor.html) | [Outbound Workflow](https://python.temporal.io/temporalio.worker.WorkflowOutboundInterceptor.html) | [Inbound Activity](https://python.temporal.io/temporalio.worker.ActivityInboundInterceptor.html) | [Outbound Activity](https://python.temporal.io/temporalio.worker.ActivityOutboundInterceptor.html) |
32+
| --- | --- | --- | --- | --- | --- |
33+
| **Description** | Wraps calls from your application to the Temporal Client to start a Workflow or send [Messages](/encyclopedia/workflow-message-passing/) to it | Wraps calls arriving into a [Workflow Execution](/workflow-execution), such as executing the Workflow, handling [Messages](/encyclopedia/workflow-message-passing/) | Wraps calls a [Workflow](/workflow-definition) makes to the SDK, such as scheduling [Activities](/activities), starting [Child Workflows](/child-workflows), and invoking [Nexus Operations](/nexus) | Wraps calls arriving into an [Activity Execution](/activity-execution) | Wraps calls an [Activity](/activities) makes to the SDK, such as sending [Heartbeats](/encyclopedia/detecting-activity-failures#activity-heartbeat) and reading Activity info |
34+
| **Runs on** | Client | Worker (Workflow sandbox) | Worker (Workflow sandbox) | Worker (Activity context) | Worker (Activity context) |
35+
| **Example methods** | `start_workflow()`, `signal_workflow()`, `list_workflows()` | `execute_workflow()`, `handle_query()`, `handle_signal()`, `handle_update_handler()` | `start_activity()`, `start_child_workflow()`, `signal_child_workflow()`, `start_nexus_operation()` | `execute_activity()` | `info()`, `heartbeat()` |
3436

35-
This is not an exhaustive list; refer to the
36-
[Python SDK methods](https://python.temporal.io/temporalio.client.OutboundInterceptor.html) for details.
37+
These are not exhaustive lists; refer to the linked API docs for each category.
3738

38-
The first of these categories is a Client call, and the remaining 4 are Worker calls.
39+
:::warning Workflow interceptors and replay
40+
41+
Workflow inbound and outbound interceptor methods also execute during [replay](/develop/python/testing-suite#replay). Use replay-safe APIs for logging, randomness, and time in these interceptors.
42+
See [Develop Workflow logic](/develop/python/core-application#workflow-logic-requirements) for details.
43+
44+
If you want to write generic code shared by all inbound Workflow call handlers but want to skip read-only operations, check `workflow.unsafe.is_read_only()`.
45+
46+
Activity and Client interceptors are not affected by replay.
47+
48+
:::
49+
50+
## Register an Interceptor {#register}
51+
52+
Registering an interceptor means supplying an interceptor instance to the SDK so Temporal can invoke it when matching
53+
Client or Worker calls occur. Once registered, the interceptor runs as part of the call path and can observe or modify
54+
request and response data.
55+
56+
### Register on the Client
57+
58+
Pass interceptors in the `interceptors` argument of `Client.connect()`. Client interceptors modify outbound calls such
59+
as starting and signaling Workflows.
60+
61+
```python
62+
client = await Client.connect(
63+
"localhost:7233",
64+
interceptors=[TracingInterceptor()],
65+
)
66+
```
67+
68+
The `interceptors` list can contain multiple interceptors.
69+
In this case they form a chain: a method implemented on an interceptor instance in the list can perform side effects, and modify the data, before passing it on to the corresponding method on the next interceptor in the list.
70+
71+
### Register via a Plugin
72+
73+
If you're building a reusable library or want to bundle interceptors with other primitives, you can register them through a [Plugin](/develop/plugins-guide#interceptors).
74+
75+
### Register on the Worker only
76+
77+
If your interceptor doesn't affect the Client, you can pass interceptors in the `interceptors` argument of `Worker()`.
78+
Worker interceptors modify inbound and outbound Workflow and Activity calls.
79+
80+
```python
81+
worker = Worker(
82+
client,
83+
task_queue="my-task-queue",
84+
interceptors=[SomeWorkerInterceptor()],
85+
# ...
86+
)
87+
```
88+
89+
:::note
90+
91+
If your interceptor class inherits from both `client.Interceptor` and `worker.Interceptor`, pass it to
92+
`Client.connect()` rather than the `Worker()` constructor. The Worker will use interceptors from its underlying Client
93+
automatically.
94+
95+
:::
3996

4097
## Register an Interceptor {#register}
4198

@@ -153,11 +210,10 @@ and `worker.Interceptor` as above, since their method sets do not overlap.
153210

154211
You can then [register](#register) this interceptor in your client/starter code.
155212

156-
Your interceptor classes need not implement every method; the default implementation is always to pass the data on to
157-
the next method in the interceptor chain. During execution, when the SDK encounters an Inbound Activity call, it will
158-
look to the first Interceptor instance, get hold of the appropriate intercepted method, and call it. The intercepted
159-
method will perform its function then call the same method on the next Interceptor in the chain. At the end of the chain
160-
the SDK will call the "real" SDK method.
213+
Your interceptor classes need not implement every method; the default implementation is always to pass the data on to the next method in the interceptor chain.
214+
During execution, when the SDK encounters an Inbound Activity call, it will look to the first Interceptor instance, get hold of the appropriate intercepted method, and call it.
215+
The intercepted method will perform its function then call the same method on the next Interceptor in the chain.
216+
At the end of the chain the SDK will call the "real" SDK method.
161217

162218
### Implementing Worker call Interceptors
163219

0 commit comments

Comments
 (0)