Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Manual edits
  • Loading branch information
donald-pinckney committed Mar 17, 2026
commit b78919c13e353c01027674799a81575ad8628fde
6 changes: 5 additions & 1 deletion references/go/advanced-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,11 @@ handle.Delete(ctx)

## Async Activity Completion

For activities that complete asynchronously (human tasks, external callbacks).
For activities that complete asynchronously (e.g., human tasks, external callbacks).
If you configure a heartbeat_timeout on this activity, the external completer is responsible for sending heartbeats via the async handle.
If you do NOT set a heartbeat_timeout, no heartbeats are required.

**Note:** If the external system that completes the asynchronous action can reliably be trusted to do the task and Signal back with the result, and it doesn't need to Heartbeat or receive Cancellation, then consider using **signals** instead.

**Step 1: Return `activity.ErrResultPending` from the activity.**

Expand Down
2 changes: 1 addition & 1 deletion references/go/determinism-protection.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Overview

The Go SDK has no runtime sandbox. Determinism is enforced by **developer convention** and **optional static analysis**. Unlike the Python and TypeScript SDKs, the Go SDK will not intercept or replace non-deterministic calls at runtime. The Go SDK does perform a limited runtime command-ordering check, but catching non-deterministic code before deployment requires the `workflowcheck` tool.
The Go SDK has no runtime sandbox. Determinism is enforced by **developer convention** and **optional static analysis**. Unlike the Python and TypeScript SDKs, the Go SDK will not intercept or replace non-deterministic calls at runtime. The Go SDK does perform a limited runtime command-ordering check, but catching non-deterministic code before deployment requires the `workflowcheck` tool and testing, in particular replay tests (see `references/go/testing`).

## workflowcheck Static Analysis

Expand Down
23 changes: 2 additions & 21 deletions references/go/determinism.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,12 @@

## Overview

The Go SDK has NO runtime sandbox (unlike Python/TypeScript). Workflows must be deterministic for replay, and determinism is enforced entirely by developer convention and optional static analysis via the `workflowcheck` tool.
The Go SDK has NO runtime sandbox (unlike Python/TypeScript). Workflows must be deterministic for replay, and determinism is enforced entirely by developer convention and optional static analysis via the `workflowcheck` tool (see `references/go/determinism-protection.md`).

## Why Determinism Matters: History Replay

Temporal provides durable execution through **History Replay**. When a Worker restores workflow state, it re-executes workflow code from the beginning. This requires the code to be **deterministic**. See `references/core/determinism.md` for a deep explanation.

## SDK Protection

Go relies on **convention** and the `workflowcheck` static analysis tool (`go.temporal.io/sdk/contrib/tools/workflowcheck`). There is no runtime sandbox that intercepts or replaces non-deterministic calls. The Go SDK does perform a limited runtime check that detects reordering of commands (e.g., `workflow.ExecuteActivity`, `workflow.Sleep`, `workflow.NewTimer`), but it does not catch all non-deterministic code.

## Forbidden Operations
Comment thread
donald-pinckney marked this conversation as resolved.

Do not use any of the following in workflow code:
Expand Down Expand Up @@ -42,22 +38,7 @@ Do not use any of the following in workflow code:

## Testing Replay Compatibility

Use `worker.WorkflowReplayer` to verify code changes are compatible with existing histories:

```go
replayer := worker.NewWorkflowReplayer()
replayer.RegisterWorkflow(MyWorkflow)
err := replayer.ReplayWorkflowHistoryFromJSONFile(nil, "history.json")
if err != nil {
// Non-determinism detected
}
```

Get history JSON from the CLI:

```bash
temporal workflow show --workflow-id my-workflow-id --output json > history.json
```
Use `worker.WorkflowReplayer` to verify code changes are compatible with existing histories. See the Workflow Replay Testing section of `references/go/testing.md`

## Best Practices

Expand Down
1 change: 1 addition & 0 deletions references/go/go.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,3 +239,4 @@ See `references/go/testing.md` for info on writing tests.
- **`references/go/advanced-features.md`** - Schedules, worker tuning, and more
- **`references/go/data-handling.md`** - Data converters, payload codecs, encryption
- **`references/go/versioning.md`** - Patching API (`workflow.GetVersion`), Worker Versioning
- **`references/python/determinism-protection.md`** - Information on **`workflowcheck`** tool to help statically check for determinism issues.
87 changes: 36 additions & 51 deletions references/go/gotchas.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,22 +109,48 @@ Use the `workflowcheck` static analysis tool to catch non-deterministic calls. F
## Wrong Retry Classification

**Example:** Transient network errors should be retried. Authentication errors should not be.
See `references/go/error-handling.md` for detailed guidance on error classification and retry policies.

## Heartbeating

### Forgetting to Heartbeat Long Activities

```go
// BAD: Retrying a permanent error
return temporal.NewApplicationError("User not found", "NotFoundError")
// This will retry indefinitely!
// BAD - No heartbeat, can't detect stuck activities or receive cancellation
func ProcessLargeFile(ctx context.Context, path string) error {
for _, chunk := range readChunks(path) {
process(chunk) // Takes hours, no heartbeat
}
return nil
}

// GOOD: Mark permanent errors as non-retryable
return temporal.NewNonRetryableApplicationError("User not found", "NotFoundError", nil)
// GOOD - Regular heartbeats with progress
func ProcessLargeFile(ctx context.Context, path string) error {
for i, chunk := range readChunks(path) {
activity.RecordHeartbeat(ctx, fmt.Sprintf("Processing chunk %d", i))
process(chunk)
}
return nil
}
```

### Heartbeat Timeout Too Short

```go
// BAD - Heartbeat timeout shorter than processing time
ao := workflow.ActivityOptions{
StartToCloseTimeout: 30 * time.Minute,
HeartbeatTimeout: 10 * time.Second, // Too short!
}

// GOOD: Or use NonRetryableErrorTypes in RetryPolicy
RetryPolicy: &temporal.RetryPolicy{
NonRetryableErrorTypes: []string{"NotFoundError", "ValidationError"},
},
// GOOD - Heartbeat timeout allows for processing variance
ao := workflow.ActivityOptions{
StartToCloseTimeout: 30 * time.Minute,
HeartbeatTimeout: 2 * time.Minute,
}
```

See `references/go/error-handling.md` for detailed guidance on error classification and retry policies.
Set heartbeat timeout as high as acceptable for your use case -- each heartbeat counts as an action.

## Cancellation

Expand Down Expand Up @@ -186,47 +212,6 @@ func LongActivity(ctx context.Context) error {
}
```

## Heartbeating

### Forgetting to Heartbeat Long Activities

```go
// BAD - No heartbeat, can't detect stuck activities or receive cancellation
func ProcessLargeFile(ctx context.Context, path string) error {
for _, chunk := range readChunks(path) {
process(chunk) // Takes hours, no heartbeat
}
return nil
}

// GOOD - Regular heartbeats with progress
func ProcessLargeFile(ctx context.Context, path string) error {
for i, chunk := range readChunks(path) {
activity.RecordHeartbeat(ctx, fmt.Sprintf("Processing chunk %d", i))
process(chunk)
}
return nil
}
```

### Heartbeat Timeout Too Short

```go
// BAD - Heartbeat timeout shorter than processing time
ao := workflow.ActivityOptions{
StartToCloseTimeout: 30 * time.Minute,
HeartbeatTimeout: 10 * time.Second, // Too short!
}

// GOOD - Heartbeat timeout allows for processing variance
ao := workflow.ActivityOptions{
StartToCloseTimeout: 30 * time.Minute,
HeartbeatTimeout: 2 * time.Minute,
}
```

Set heartbeat timeout as high as acceptable for your use case -- each heartbeat counts as an action.

## Testing

### Not Testing Failures
Expand Down
11 changes: 10 additions & 1 deletion references/go/observability.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ func MyWorkflow(ctx workflow.Context, input string) (string, error) {
}
```

The workflow logger automatically:
- Suppresses duplicate logs during replay
- Includes workflow context (workflow ID, run ID, etc.)

### Activity Logging

Use `activity.GetLogger(ctx)` for context-aware activity logging:
Expand All @@ -40,6 +44,11 @@ func MyActivity(ctx context.Context, input string) (string, error) {
}
```

Activity logger includes:
- Activity ID, type, and task queue
- Workflow ID and run ID
- Attempt number (for retries)

### Adding Persistent Fields

Use `log.With` to create a logger with key-value pairs included in every entry:
Expand Down Expand Up @@ -133,7 +142,7 @@ Key SDK metrics:

## Search Attributes (Visibility)

Use `workflow.UpsertSearchAttributes(ctx, attrs)` for runtime indexing. Custom search attributes enable filtering in the UI and `temporal workflow list`. See `references/go/data-handling.md` for full details.
See the Search Attributes section of `references/go/data-handling.md`

## Best Practices

Expand Down
4 changes: 2 additions & 2 deletions references/go/patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ func ParentWorkflow(ctx workflow.Context, orders []Order) ([]string, error) {
}
```

## Child Workflow Options
### Child Workflow Options

```go
import enumspb "go.temporal.io/api/enums/v1"
Expand Down Expand Up @@ -322,7 +322,7 @@ return "", workflow.NewContinueAsNewError(ctx, LongRunningWorkflow, state)

## Saga Pattern (Compensations)

**Important:** Compensation activities should be idempotent -- they may be retried.
**Important:** Compensation activities should be idempotent -- they may be retried (as with ALL activities).

```go
func OrderWorkflow(ctx workflow.Context, order Order) (string, error) {
Expand Down
4 changes: 2 additions & 2 deletions references/go/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,8 @@ func Test_ReplayFromFile(t *testing.T) {
}
```

Export history via CLI: `temporal workflow show --workflow-id <id> --output json > history.json`

**Replay from a programmatically fetched history:**

```go
Expand All @@ -205,8 +207,6 @@ func Test_ReplayFromServer(t *testing.T) {
}
```

Export history via CLI: `temporal workflow show --workflow-id <id> --output json > history.json`

## Activity Testing

Test Activities in isolation using `TestActivityEnvironment`. No Worker or Workflow needed.
Expand Down