Files
Donald Pinckney b5719bc143 PR Tracking Initial Release (#4)
* Add initial skill for testing, which is simply Steve's skill (#1)

* Add initial skill for testing, which is simply Steve's skill

* Rename skill to 'temporal-dev' and update version

Updated skill name and version for Temporal Python.

* Use claude to merge Steve's, Max's, and Mason's skills.  (#2)

* Use claude to merge Steve's, Max's, and Mason's skills. Did a review pass using claude's skill devlopment skills

* Add missing things from Steve

* trigger tweaks

* Add in common gotchas from Johann

* add simple feedback mechanism (#3)

* Change skill name to kebab-case, for compatibility with Amp and Cline (#7)

* Clean up references/core/ai-integration.md

* Clean up references/core/common-gotchas.md

* Clean up references/core/common-gotchas.md

* Clean up references/core/determinism.md

* Clean up references/core/determinism.md

* Update error-reference.md

* Update interactive-workflows.md

* Clean up patterns.md

* Cut shell scripts

* Edit troubleshooting.md

* remove interceptors for now

* remove dynamic workflows

* clarify on heartbeating of async activity completions, and prompt it a bit in relation to signals

* Improve references/python/advanced-features.md

* Use explicit namespace in connect

* remove duplicated content from determinism.md, clean up

* Improve references/python/data-handling.md

* Prefer start_to_close_timeout

* don't explicitely provide defaults for retry policies

* error-handling.md cleanup

* move idempotency patterns to patterns.md

* remove multi-param activities

* small edits

* Unify sandbox stuff into one file

* local activities aren't experimental

* Clean up references/python/sync-vs-async.md

* Cleanup observability.md, remove duplicated search attributes

* Cut otel for now

* cut a lot of duplicate stuff from python gotchas, address comments

* de-duplicate content

* Lots of improvements to testing

* cleanup to top level of skill (like CLI install instructions), and to top-level of python

* Improve patterns.md

* clean up ai-patterns.md

* Update readme with installation instructions

* remove ts directory

* De-couple core from python and TypeScript as much as possible

* Remove TypeScript hints

* add prompting for feedback at startup - wait for ethan on slack channel

* shorten url

* Update slack channel

* Automated pass over on python cleanup & deduplication

* Remove multi-patching from Python, since its obvious, dont waste tokens on it. (#34)

* Add TypeScript (#31)

Adds initial support for TypeScript to the skill

---------

Co-authored-by: James Watkins-Harvey <mjameswh@users.noreply.github.com>
Co-authored-by: Chris Olszewski <chrisdolszewski@gmail.com>

* Fix typos and reference links (#36)

* Fix typos and reference links

* 2 more typo fixes

* quick edit to readme (#37)

* Fix saga compensations to run under cancellation protection (#43)

When a workflow is cancelled mid-saga, compensations must run in a
cancellation-protected scope, otherwise they are immediately cancelled
before they can execute.

- Python: wrap compensation loop in asyncio.shield() so it runs even
  when the workflow receives a CancelledError
- TypeScript: wrap compensation loop in CancellationScope.nonCancellable()
  so it runs even when the root scope is cancelled (per official docs:
  "Cleanup logic must be in a nonCancellable scope")
- TypeScript: also fix compensation registration order — register BEFORE
  calling the activity (was already correct in Python)

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* Update readme for public preview (#45)

* a few more readme tweaks (#46)

* Add MIT License to the project (#47)

* Add Go (supersedes other PR) (#38)

* progress on go

* Go translation workflow completed.

* missed a few spots

* Manual edits

* Address feedback

* Add gotcha about anonymous local activities

* Sample code for payload converter

* clarify sdk protection mechanisms

* Setup CODEOWNERS to AI SDK team (#48)

* Align version number in SKILL.md and plugin.json. (#49)

---------

Co-authored-by: James Watkins-Harvey <mjameswh@users.noreply.github.com>
Co-authored-by: Chris Olszewski <chrisdolszewski@gmail.com>
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 17:36:15 -04:00

7.4 KiB

Go Gotchas

Go-specific mistakes and anti-patterns. See also Common Gotchas for language-agnostic concepts.

Goroutines and Concurrency

Using Native Go Concurrency Primitives

The Problem: Native go, chan, and select are non-deterministic and will cause replay failures.

// BAD - Native goroutine
func MyWorkflow(ctx workflow.Context) error {
	go func() { // Non-deterministic!
		// do work
	}()
	return nil
}

// GOOD - Use workflow.Go
func MyWorkflow(ctx workflow.Context) error {
	workflow.Go(ctx, func(gCtx workflow.Context) {
		// do work
	})
	return nil
}
// BAD - Native channel
func MyWorkflow(ctx workflow.Context) error {
	ch := make(chan string) // Non-deterministic!
	return nil
}

// GOOD - Use workflow.Channel
func MyWorkflow(ctx workflow.Context) error {
	ch := workflow.NewChannel(ctx)
	return nil
}
// BAD - Native select
select {
case val := <-ch1:
	// handle
case val := <-ch2:
	// handle
}

// GOOD - Use workflow.Selector
selector := workflow.NewSelector(ctx)
selector.AddReceive(ch1, func(c workflow.ReceiveChannel, more bool) {
	var val string
	c.Receive(ctx, &val)
	// handle
})
selector.AddReceive(ch2, func(c workflow.ReceiveChannel, more bool) {
	var val string
	c.Receive(ctx, &val)
	// handle
})
selector.Select(ctx)

Non-Deterministic Operations

Map Iteration

// BAD - Map range order is randomized
for k, v := range myMap {
	// Non-deterministic order!
}

// GOOD - Sort keys first
keys := make([]string, 0, len(myMap))
for k := range myMap {
	keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
	v := myMap[k]
	// Deterministic order
}

Time and Randomness

// BAD
t := time.Now()           // System clock, non-deterministic
time.Sleep(time.Second)   // Not replay-safe
r := rand.Intn(100)       // Non-deterministic

// GOOD
t := workflow.Now(ctx)                     // Deterministic
workflow.Sleep(ctx, time.Second)           // Durable timer
encoded := workflow.SideEffect(ctx, func(ctx workflow.Context) interface{} {
	return rand.Intn(100)
})
var r int
encoded.Get(&r)

Use the workflowcheck static analysis tool to catch non-deterministic calls. For false positives, annotate with //workflowcheck:ignore on the line above.

Anonymous Functions as Local Activities

The Problem: The Go SDK derives the local activity name from the function. Anonymous functions get a non-deterministic name that can change across builds, causing replay failures.

// BAD - anonymous function: name is non-deterministic
workflow.ExecuteLocalActivity(ctx, func(ctx context.Context) (string, error) {
    return "result", nil
})

// GOOD - named function: stable, deterministic name
func QuickLookup(ctx context.Context) (string, error) {
    return "result", nil
}

workflow.ExecuteLocalActivity(ctx, QuickLookup)

Always use named functions for local activities (and regular activities).

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

// 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

// 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.

Cancellation

Not Handling Workflow Cancellation

// BAD - Cleanup doesn't run on cancellation
func BadWorkflow(ctx workflow.Context) error {
	_ = workflow.ExecuteActivity(ctx, AcquireResource).Get(ctx, nil)
	_ = workflow.ExecuteActivity(ctx, DoWork).Get(ctx, nil)
	_ = workflow.ExecuteActivity(ctx, ReleaseResource).Get(ctx, nil) // Never runs if cancelled!
	return nil
}

// GOOD - Use defer with NewDisconnectedContext for cleanup
func GoodWorkflow(ctx workflow.Context) error {
	defer func() {
		if !errors.Is(ctx.Err(), workflow.ErrCanceled) {
			return
		}
		newCtx, _ := workflow.NewDisconnectedContext(ctx)
		_ = workflow.ExecuteActivity(newCtx, ReleaseResource).Get(newCtx, nil)
	}()

	err := workflow.ExecuteActivity(ctx, AcquireResource).Get(ctx, nil)
	if err != nil {
		return err
	}
	return workflow.ExecuteActivity(ctx, DoWork).Get(ctx, nil)
}

Not Handling Activity Cancellation

Activities must opt in to receive cancellation. This requires:

  1. Heartbeating - Cancellation is delivered via heartbeat
  2. Checking ctx.Done() - Detect when cancellation arrives
// BAD - Activity ignores cancellation
func LongActivity(ctx context.Context) error {
	doExpensiveWork() // Runs to completion even if cancelled
	return nil
}

// GOOD - Heartbeat and check ctx.Done()
func LongActivity(ctx context.Context) error {
	for i, item := range items {
		select {
		case <-ctx.Done():
			cleanup()
			return ctx.Err()
		default:
			activity.RecordHeartbeat(ctx, fmt.Sprintf("Processing item %d", i))
			process(item)
		}
	}
	return nil
}

Testing

Not Testing Failures

It is important to make sure workflows work as expected under failure paths in addition to happy paths. Please see references/go/testing.md for more info.

Not Testing Replay

Replay tests help you test that you do not have hidden sources of non-determinism bugs in your workflow code, and should be considered in addition to standard testing. Please see references/go/testing.md for more info.

Timers and Sleep

Using time.Sleep Instead of workflow.Sleep

// BAD: time.Sleep is not deterministic during replay
func BadWorkflow(ctx workflow.Context) error {
	time.Sleep(60 * time.Second) // Non-deterministic!
	return nil
}

// GOOD: Use workflow.Sleep for deterministic timers
func GoodWorkflow(ctx workflow.Context) error {
	workflow.Sleep(ctx, 60*time.Second) // Deterministic
	return nil
}

Using time.After Instead of workflow.NewTimer

// BAD: time.After is not replay-safe
func BadWorkflow(ctx workflow.Context) error {
	<-time.After(5 * time.Minute) // Non-deterministic!
	return nil
}

// GOOD: Use workflow.NewTimer for durable timers
func GoodWorkflow(ctx workflow.Context) error {
	timer := workflow.NewTimer(ctx, 5*time.Minute)
	_ = timer.Get(ctx, nil) // Deterministic, durable
	return nil
}

Using time.Now() Instead of workflow.Now()

// BAD: time.Now() differs between execution and replay
deadline := time.Now().Add(24 * time.Hour)

// GOOD: workflow.Now() is replay-safe
deadline := workflow.Now(ctx).Add(24 * time.Hour)

Why this matters: time.Now(), time.Sleep(), and time.After() use the system clock, which differs between original execution and replay. The workflow.* equivalents create durable, deterministic entries in the event history.