Skip to content

Commit 3896154

Browse files
authored
Document replay-safe Workflow APIs per SDK (#5054)
* Document replay-safe Workflow APIs per SDK Add a heading per topic under the Workflow logic requirements section so readers can find the replay-safe alternative for what they need. - Ruby and .NET: add Logging, Random/UUIDs, Current time, and Detecting replay sections. Neither page previously named any replacement API. - Go: add the same four sections. Go has no built-in seeded random or UUID helper, so that section shows the Side Effect approach. - Java: add the same four sections, and point the existing "non-deterministic functions" bullet at the new one. - TypeScript: lead the Random/UUIDs section with the SDK's own uuid4() instead of the uuid npm package. Each documented API was run against a dev server and its history replayed through the SDK replayer. * Remove counter-examples from Random numbers sections Show only the recommended API. Also removes the pre-existing bad example from the Python page. * Use WorkflowUnsafe.isReplaying() in Java replay detection Workflow.isReplaying() is deprecated in favor of the unsafe package method, which also matches the other SDKs.
1 parent 44e98d0 commit 3896154

6 files changed

Lines changed: 277 additions & 9 deletions

File tree

docs/develop/dotnet/workflows/basics.mdx

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,77 @@ This means there are several things Workflows shouldn't do such as:
8686
- Make any random calls
8787
- Make any not-guaranteed-deterministic calls (e.g. iterating over a dictionary)
8888

89+
The SDK provides replay-safe alternatives for common needs.
90+
91+
### Logging
92+
93+
Use [`Workflow.Logger`](https://dotnet.temporal.io/api/Temporalio.Workflows.Workflow.html#Temporalio_Workflows_Workflow_Logger)
94+
instead of `Console.WriteLine` or a logger you resolve yourself.
95+
The SDK logger appends Workflow details to every log and skips logging during replay:
96+
97+
```csharp
98+
[Workflow]
99+
public class MyWorkflow
100+
{
101+
[WorkflowRun]
102+
public async Task<string> RunAsync(string name)
103+
{
104+
Workflow.Logger.LogInformation("Starting workflow for {Name}", name);
105+
// ...
106+
}
107+
}
108+
```
109+
110+
For logger configuration, see [Observability: Log from a Workflow](/develop/dotnet/platform/observability#logging).
111+
112+
### Random numbers and GUIDs
113+
114+
Use [`Workflow.Random`](https://dotnet.temporal.io/api/Temporalio.Workflows.Workflow.html#Temporalio_Workflows_Workflow_Random)
115+
to get a deterministic random instance seeded per Workflow Execution, and
116+
[`Workflow.NewGuid()`](https://dotnet.temporal.io/api/Temporalio.Workflows.Workflow.html#Temporalio_Workflows_Workflow_NewGuid)
117+
instead of `Guid.NewGuid()`:
118+
119+
```csharp
120+
var value = Workflow.Random.Next(1, 100);
121+
var uniqueId = Workflow.NewGuid();
122+
```
123+
124+
### Current time
125+
126+
Use [`Workflow.UtcNow`](https://dotnet.temporal.io/api/Temporalio.Workflows.Workflow.html#Temporalio_Workflows_Workflow_UtcNow)
127+
instead of `DateTime.Now` or `DateTime.UtcNow`. It returns the time of the last Workflow Task, which is consistent
128+
across replays:
129+
130+
```csharp
131+
var currentTime = Workflow.UtcNow;
132+
```
133+
134+
To wait, use `Workflow.DelayAsync` instead of `Task.Delay` or `Thread.Sleep`.
135+
136+
### Detecting replay (advanced)
137+
138+
Use [`Workflow.Unsafe.IsReplaying`](https://dotnet.temporal.io/api/Temporalio.Workflows.Workflow.Unsafe.html#Temporalio_Workflows_Workflow_Unsafe_IsReplaying)
139+
to guard code that should only run on the first execution, such as emitting metrics or sending external notifications
140+
from an [Interceptor](/develop/dotnet/workers/interceptors).
141+
142+
:::caution
143+
144+
Never use this to affect Workflow business logic. Branching on replay status breaks determinism.
145+
146+
:::
147+
148+
```csharp
149+
if (!Workflow.Unsafe.IsReplaying)
150+
{
151+
EmitMetric("workflow_started", 1);
152+
}
153+
```
154+
155+
If your goal is to always take action when something new is happening, check that
156+
[`Workflow.Unsafe.IsReplayingHistoryEvents`](https://dotnet.temporal.io/api/Temporalio.Workflows.Workflow.Unsafe.html#Temporalio_Workflows_Workflow_Unsafe_IsReplayingHistoryEvents)
157+
is false instead. That is false during read-only operations like Queries and Update validators. This is what the SDK's
158+
built-in logger and tracing interceptors use internally.
159+
89160
### .NET Task Determinism
90161

91162
Some calls in .NET do unsuspecting non-deterministic things and are easy to accidentally use.

docs/develop/go/workflows/basics.mdx

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,3 +224,63 @@ The Temporal Go SDK has APIs to handle equivalent Go constructs:
224224
Learn more on the [Go SDK Selectors](https://legacy-documentation-sdks.temporal.io/go/selectors) page.
225225
- `workflow.Context` This is a replacement for `context.Context`.
226226
See [Tracing](/develop/go/platform/observability#tracing) for more information about context propagation.
227+
228+
#### Logging
229+
230+
Use [`workflow.GetLogger(ctx)`](https://pkg.go.dev/go.temporal.io/sdk/workflow#GetLogger) instead of the standard
231+
`log` package or `fmt.Println`. The SDK logger skips log messages during replay to avoid duplicates:
232+
233+
```go
234+
func MyWorkflow(ctx workflow.Context, name string) (string, error) {
235+
logger := workflow.GetLogger(ctx)
236+
logger.Info("Starting workflow", "name", name)
237+
// ...
238+
}
239+
```
240+
241+
For logger configuration, see [Observability: Log from a Workflow](/develop/go/platform/observability#logging).
242+
243+
#### Random numbers and UUIDs
244+
245+
The Go SDK does not provide a seeded random source or a UUID helper. Generate these inside a
246+
[Side Effect](/develop/go/workflows/side-effects), which records the result in the Event History and returns the
247+
recorded value on replay:
248+
249+
```go
250+
var id string
251+
encodedID := workflow.SideEffect(ctx, func(ctx workflow.Context) interface{} {
252+
return uuid.New().String()
253+
})
254+
encodedID.Get(&id)
255+
```
256+
257+
An Activity works for this too, and is the better choice when the value comes from an external system. A Side Effect is
258+
cheaper for purely local generation.
259+
260+
#### Current time
261+
262+
Use [`workflow.Now(ctx)`](https://pkg.go.dev/go.temporal.io/sdk/workflow#Now) instead of `time.Now()`. It returns the
263+
time of the last Workflow Task, which is consistent across replays:
264+
265+
```go
266+
currentTime := workflow.Now(ctx)
267+
```
268+
269+
To wait, use [`workflow.Sleep(ctx, d)`](https://pkg.go.dev/go.temporal.io/sdk/workflow#Sleep) instead of `time.Sleep`.
270+
271+
#### Detecting replay (advanced)
272+
273+
Use [`workflow.IsReplaying(ctx)`](https://pkg.go.dev/go.temporal.io/sdk/workflow#IsReplaying) to guard code that should
274+
only run on the first execution, such as emitting metrics or sending external notifications from an Interceptor.
275+
276+
:::caution
277+
278+
Never use this to affect Workflow business logic. Branching on replay status breaks determinism.
279+
280+
:::
281+
282+
```go
283+
if !workflow.IsReplaying(ctx) {
284+
emitMetric("workflow_started", 1)
285+
}
286+
```

docs/develop/java/workflows/basics.mdx

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ When defining Workflows using the Temporal Java SDK, the Workflow code must be w
253253
The following constraints apply when writing Workflow Definitions:
254254

255255
- Do not use mutable global variables in your Workflow implementations. This will ensure that multiple Workflow instances are fully isolated.
256-
- Workflow code must be deterministic. If you need to call non-deterministic functions (such as non-seeded random or `UUID.randomUUID()`) directly from the Workflow code, the Temporal SDK provides specific API for calling non-deterministic code in your Workflows.
256+
- Workflow code must be deterministic. If you need to call non-deterministic functions (such as non-seeded random or `UUID.randomUUID()`) directly from the Workflow code, the Temporal SDK provides replay-safe replacements. See [Random numbers and UUIDs](#random-numbers-and-uuids).
257257
- For operations like calling external APIs, invoking LLMs, querying databases, or performing I/O, use Activities. Activities run outside Workflow replay and are retried reliably.
258258
- Use Temporal-provided functions instead of that rely on system time. For example, use only `Workflow.currentTimeMillis()` to get the current time inside a Workflow.
259259
- Use `Async.function` or `Async.procedure`, provided by the Temporal SDK, to execute code asynchronously instead of native Java `Thread` or any other multi-threaded classes like `ThreadPoolExecutor`.
@@ -265,4 +265,71 @@ The following constraints apply when writing Workflow Definitions:
265265
- Do not access configuration APIs directly from a Workflow because changes in the configuration might affect a Workflow Execution path. Instead, pass it as an argument to a Workflow function or use an Activity to load it.
266266
- Use `DynamicWorkflow` when you need a default Workflow that can handle all Workflow Types that are not registered with a Worker. A single implementation can implement a Workflow Type which by definition is dynamically loaded from some external source. All standard `WorkflowOptions` and determinism rules apply to Dynamic Workflow implementations.
267267

268+
The SDK provides replay-safe alternatives for common needs.
269+
270+
### Logging
271+
272+
Use [`Workflow.getLogger()`](https://www.javadoc.io/doc/io.temporal/temporal-sdk/latest/io/temporal/workflow/Workflow.html)
273+
instead of `System.out.println` or a logger you create yourself. The SDK logger skips log messages during replay to
274+
avoid duplicates:
275+
276+
```java
277+
public class MyWorkflowImpl implements MyWorkflow {
278+
private static final Logger logger = Workflow.getLogger(MyWorkflowImpl.class);
279+
280+
@Override
281+
public String execute(String name) {
282+
logger.info("Starting workflow for {}", name);
283+
// ...
284+
}
285+
}
286+
```
287+
288+
For logger configuration, see [Observability: Log from a Workflow](/develop/java/platform/observability#logging).
289+
290+
### Random numbers and UUIDs
291+
292+
Use [`Workflow.newRandom()`](https://www.javadoc.io/doc/io.temporal/temporal-sdk/latest/io/temporal/workflow/Workflow.html)
293+
to get a `Random` instance seeded per Workflow Execution, and
294+
[`Workflow.randomUUID()`](https://www.javadoc.io/doc/io.temporal/temporal-sdk/latest/io/temporal/workflow/Workflow.html)
295+
instead of `UUID.randomUUID()`:
296+
297+
```java
298+
int value = Workflow.newRandom().nextInt(100);
299+
UUID uniqueId = Workflow.randomUUID();
300+
```
301+
302+
### Current time
303+
304+
Use [`Workflow.currentTimeMillis()`](https://www.javadoc.io/doc/io.temporal/temporal-sdk/latest/io/temporal/workflow/Workflow.html)
305+
instead of `System.currentTimeMillis()` or `Instant.now()`. It returns the time of the last Workflow Task, which is
306+
consistent across replays:
307+
308+
```java
309+
long currentTime = Workflow.currentTimeMillis();
310+
```
311+
312+
To wait, use [`Workflow.sleep()`](https://www.javadoc.io/doc/io.temporal/temporal-sdk/latest/io/temporal/workflow/Workflow.html)
313+
instead of `Thread.sleep()`.
314+
315+
### Detecting replay (advanced)
316+
317+
Use [`WorkflowUnsafe.isReplaying()`](https://www.javadoc.io/doc/io.temporal/temporal-sdk/latest/io/temporal/workflow/unsafe/WorkflowUnsafe.html)
318+
to guard code that should only run on the first execution, such as emitting metrics or sending external notifications
319+
from an Interceptor. `Workflow.isReplaying()` is deprecated in favor of this method.
320+
321+
:::caution
322+
323+
Never use this to affect Workflow business logic. Branching on replay status breaks determinism.
324+
325+
:::
326+
327+
```java
328+
import io.temporal.workflow.unsafe.WorkflowUnsafe;
329+
330+
if (!WorkflowUnsafe.isReplaying()) {
331+
emitMetric("workflow_started", 1);
332+
}
333+
```
334+
268335
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)

docs/develop/python/workflows/basics.mdx

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -202,13 +202,8 @@ Use [`workflow.random()`](https://python.temporal.io/temporalio.workflow.html#ra
202202
For UUIDs, use [`workflow.uuid4()`](https://python.temporal.io/temporalio.workflow.html#uuid4) instead of `uuid.uuid4()`:
203203

204204
```python
205-
# Good - deterministic across replays
206205
value = workflow.random().randint(1, 100)
207206
unique_id = workflow.uuid4()
208-
209-
# Bad - different result on every replay
210-
import random
211-
value = random.randint(1, 100)
212207
```
213208

214209
#### Current time

docs/develop/ruby/workflows/basics.mdx

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,75 @@ Ruby Workflows. This means there are several things Workflows cannot do such as:
9898
To prevent illegal Workflow calls, a call tracer is put on the Workflow thread that raises an exception if any illegal
9999
calls are made.
100100
Which calls are illegal is configurable in the Worker options.
101+
102+
The SDK provides replay-safe alternatives for common needs.
103+
104+
### Logging
105+
106+
Use [`Temporalio::Workflow.logger`](https://ruby.temporal.io/Temporalio/Workflow.html#logger-class_method) instead of
107+
`puts` or a `Logger` you create yourself. The `Logger` class is on the default illegal call list, and the SDK logger
108+
appends Workflow details to every log and skips logging during replay:
109+
110+
```ruby
111+
class MyWorkflow < Temporalio::Workflow::Definition
112+
def execute(name)
113+
Temporalio::Workflow.logger.info("Starting workflow for #{name}")
114+
# ...
115+
end
116+
end
117+
```
118+
119+
For logger configuration, see [Observability: Log from a Workflow](/develop/ruby/platform/observability#logging).
120+
121+
### Random numbers and UUIDs
122+
123+
Use [`Temporalio::Workflow.random`](https://ruby.temporal.io/Temporalio/Workflow.html#random-class_method) to get a
124+
`Random` instance seeded per Workflow Execution. The SDK requires `random/formatter`, so this instance also has the
125+
standard library's [`Random::Formatter#uuid`](https://rubyapi.org/4.0/o/random/formatter#method-i-uuid) method. Use it
126+
instead of `SecureRandom.uuid`:
127+
128+
```ruby
129+
value = Temporalio::Workflow.random.rand(1..100)
130+
unique_id = Temporalio::Workflow.random.uuid
131+
```
132+
133+
Don't use `SecureRandom`, `Kernel#rand`, `Kernel#srand`, or `Random.new` in Workflow code. They're on the default
134+
illegal call list, and the call tracer raises a `Temporalio::Workflow::NondeterminismError` when it detects them.
135+
136+
Access the instance each time you need it rather than storing it in an instance variable. The SDK may recreate it with a
137+
different seed, such as after a Workflow reset.
138+
139+
### Current time
140+
141+
Use [`Temporalio::Workflow.now`](https://ruby.temporal.io/Temporalio/Workflow.html#now-class_method) instead of
142+
`Time.now`. It returns the UTC time of the last Workflow Task, which is consistent across replays:
143+
144+
```ruby
145+
current_time = Temporalio::Workflow.now
146+
```
147+
148+
To wait, use [`Temporalio::Workflow.sleep`](https://ruby.temporal.io/Temporalio/Workflow.html#sleep-class_method)
149+
instead of `Kernel#sleep`.
150+
151+
### Detecting replay (advanced)
152+
153+
Use [`Temporalio::Workflow::Unsafe.replaying?`](https://ruby.temporal.io/Temporalio/Workflow/Unsafe.html#replaying?-class_method)
154+
to guard code that should only run on the first execution, such as emitting metrics or sending external notifications
155+
from an Interceptor.
156+
157+
:::caution
158+
159+
Never use this to affect Workflow business logic. Branching on replay status breaks determinism.
160+
161+
:::
162+
163+
```ruby
164+
unless Temporalio::Workflow::Unsafe.replaying?
165+
emit_metric('workflow_started', 1)
166+
end
167+
```
168+
169+
If your goal is to always take action when something new is happening, check that
170+
[`Temporalio::Workflow::Unsafe.replaying_history_events?`](https://ruby.temporal.io/Temporalio/Workflow/Unsafe.html#replaying_history_events?-class_method)
171+
is false instead. That is false during read-only operations like Queries and Update validators. This is what the SDK's
172+
built-in logger uses internally.

docs/develop/typescript/workflows/basics.mdx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,16 +158,19 @@ For logger configuration, see [Observability: Log from a Workflow](/develop/type
158158

159159
#### Random numbers and UUIDs
160160

161-
`Math.random()` is replaced by a deterministic version in the sandbox, so you can use it directly. It produces the same sequence of values on replay. UUID libraries that rely on `Math.random()` (such as the `uuid` package) are also safe to use. Avoid `crypto.randomUUID()`, which is not available in the sandbox:
161+
`Math.random()` is replaced by a deterministic version in the sandbox, so you can use it directly. It produces the same sequence of values on replay.
162+
163+
For UUIDs, use [`uuid4()`](https://typescript.temporal.io/api/namespaces/workflow#uuid4) from `@temporalio/workflow`. It draws from the sandbox's deterministic random source, so it needs no extra dependency. Avoid `crypto.randomUUID()`, which is not available in the sandbox:
162164

163165
```ts
164-
import { v4 as uuid4 } from 'uuid';
166+
import { uuid4 } from '@temporalio/workflow';
165167

166-
// Safe - Math.random() is deterministic in the Workflow sandbox
167168
const value = Math.random();
168169
const id = uuid4();
169170
```
170171

172+
Third-party UUID libraries that rely on `Math.random()` (such as the `uuid` package) are also safe, but the built-in avoids the dependency.
173+
171174
#### Current time
172175

173176
`Date.now()` and `new Date()` are replaced by deterministic versions that return the time of the last Workflow Task completion. The value only advances when you `await` something (like `sleep()`):

0 commit comments

Comments
 (0)