@temporal-architect/claude-plugin 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +38 -0
  3. package/package.json +37 -0
  4. package/skills/MANIFEST.json +373 -0
  5. package/skills/MANIFEST.md +121 -0
  6. package/skills/temporal-architect/SKILL.md +99 -0
  7. package/skills/temporal-architect/reference/decomposition.md +78 -0
  8. package/skills/temporal-architect-author-go/README.md +16 -0
  9. package/skills/temporal-architect-author-go/SKILL.md +191 -0
  10. package/skills/temporal-architect-author-go/SUBAGENT_ADOPTION.md +161 -0
  11. package/skills/temporal-architect-author-go/reference/activity-call.md +73 -0
  12. package/skills/temporal-architect-author-go/reference/activity-def.md +54 -0
  13. package/skills/temporal-architect-author-go/reference/assignment.md +36 -0
  14. package/skills/temporal-architect-author-go/reference/await-all.md +104 -0
  15. package/skills/temporal-architect-author-go/reference/await-one.md +193 -0
  16. package/skills/temporal-architect-author-go/reference/await-timer.md +35 -0
  17. package/skills/temporal-architect-author-go/reference/close.md +71 -0
  18. package/skills/temporal-architect-author-go/reference/composite-patterns.md +176 -0
  19. package/skills/temporal-architect-author-go/reference/condition.md +56 -0
  20. package/skills/temporal-architect-author-go/reference/control-flow.md +151 -0
  21. package/skills/temporal-architect-author-go/reference/dependency-resolution.md +29 -0
  22. package/skills/temporal-architect-author-go/reference/detach.md +52 -0
  23. package/skills/temporal-architect-author-go/reference/heartbeat.md +84 -0
  24. package/skills/temporal-architect-author-go/reference/nexus-service-def.md +73 -0
  25. package/skills/temporal-architect-author-go/reference/nexus.md +35 -0
  26. package/skills/temporal-architect-author-go/reference/options.md +138 -0
  27. package/skills/temporal-architect-author-go/reference/promise.md +73 -0
  28. package/skills/temporal-architect-author-go/reference/proto-driven.md +197 -0
  29. package/skills/temporal-architect-author-go/reference/query-handler.md +34 -0
  30. package/skills/temporal-architect-author-go/reference/signal-handler.md +73 -0
  31. package/skills/temporal-architect-author-go/reference/three-layer-testing.md +173 -0
  32. package/skills/temporal-architect-author-go/reference/types.md +72 -0
  33. package/skills/temporal-architect-author-go/reference/update-handler.md +64 -0
  34. package/skills/temporal-architect-author-go/reference/worker.md +215 -0
  35. package/skills/temporal-architect-author-go/reference/workflow-call.md +37 -0
  36. package/skills/temporal-architect-author-go/reference/workflow-def.md +45 -0
  37. package/skills/temporal-architect-author-infra/README.md +16 -0
  38. package/skills/temporal-architect-author-infra/SKILL.md +132 -0
  39. package/skills/temporal-architect-author-infra/reference/tcld.md +112 -0
  40. package/skills/temporal-architect-author-infra/reference/terraform.md +125 -0
  41. package/skills/temporal-architect-design/README.md +16 -0
  42. package/skills/temporal-architect-design/SKILL.md +224 -0
  43. package/skills/temporal-architect-design/reference/LANGUAGE.md +5 -0
  44. package/skills/temporal-architect-design/reference/anti-patterns.md +332 -0
  45. package/skills/temporal-architect-design/reference/common-errors.md +88 -0
  46. package/skills/temporal-architect-design/reference/core-principles.md +52 -0
  47. package/skills/temporal-architect-design/reference/design-checklist.md +59 -0
  48. package/skills/temporal-architect-design/reference/namespaces.md +84 -0
  49. package/skills/temporal-architect-design/reference/notation-examples.md +304 -0
  50. package/skills/temporal-architect-design/reference/notation-reference.md +70 -0
  51. package/skills/temporal-architect-design/reference/primitives-reference.md +65 -0
  52. package/skills/temporal-architect-design/reference/project-discovery-subagent.md +80 -0
  53. package/skills/temporal-architect-design/reference/reverse-engineering.md +53 -0
  54. package/skills/temporal-architect-design/reference/twf-conventions.md +43 -0
  55. package/skills/temporal-architect-design/reference/workflow-boundaries.md +43 -0
  56. package/skills/temporal-architect-design/topics/activities-advanced.md +358 -0
  57. package/skills/temporal-architect-design/topics/activities-advanced.twf +107 -0
  58. package/skills/temporal-architect-design/topics/child-workflows.md +347 -0
  59. package/skills/temporal-architect-design/topics/child-workflows.twf +171 -0
  60. package/skills/temporal-architect-design/topics/long-running.md +230 -0
  61. package/skills/temporal-architect-design/topics/long-running.twf +100 -0
  62. package/skills/temporal-architect-design/topics/nexus.md +248 -0
  63. package/skills/temporal-architect-design/topics/nexus.twf +148 -0
  64. package/skills/temporal-architect-design/topics/patterns.md +469 -0
  65. package/skills/temporal-architect-design/topics/patterns.twf +346 -0
  66. package/skills/temporal-architect-design/topics/promises-conditions.md +179 -0
  67. package/skills/temporal-architect-design/topics/promises-conditions.twf +213 -0
  68. package/skills/temporal-architect-design/topics/signals-queries-updates.md +319 -0
  69. package/skills/temporal-architect-design/topics/signals-queries-updates.twf +234 -0
  70. package/skills/temporal-architect-design/topics/task-queues.md +205 -0
  71. package/skills/temporal-architect-design/topics/task-queues.twf +184 -0
  72. package/skills/temporal-architect-design/topics/testing.md +437 -0
  73. package/skills/temporal-architect-design/topics/testing.twf +177 -0
  74. package/skills/temporal-architect-design/topics/timers-scheduling.md +131 -0
  75. package/skills/temporal-architect-design/topics/timers-scheduling.twf +129 -0
  76. package/skills/temporal-architect-design/topics/versioning.md +434 -0
  77. package/skills/temporal-architect-design/topics/versioning.twf +174 -0
@@ -0,0 +1,54 @@
1
+ # activity definition
2
+
3
+ ## DSL
4
+
5
+ ```twf
6
+ activity ValidateOrder(order: Order) -> (ValidateResult):
7
+ result = validate(order)
8
+ return result
9
+ ```
10
+
11
+ ## Go
12
+
13
+ ```go
14
+ func ValidateOrder(ctx context.Context, order Order) (ValidateResult, error) {
15
+ result, err := validate(ctx, order)
16
+ if err != nil {
17
+ return ValidateResult{}, err
18
+ }
19
+ return result, nil
20
+ }
21
+ ```
22
+
23
+ ## Notes
24
+
25
+ - Activities use `context.Context` (stdlib), not `workflow.Context`
26
+ - Every activity returns `error` as the last return value
27
+ - No return type in DSL → `func Name(ctx context.Context, params...) error`
28
+ - Activity bodies in `.twf` are pseudocode — ask the user about real implementation logic when it's ambiguous
29
+ - In practice, activities are methods on a struct with injected dependencies — see [Activity Implementation Pattern](#activity-implementation-pattern) below
30
+
31
+ ## Activity Implementation Pattern
32
+
33
+ Activities follow a thin-wrapper pattern with dependency injection:
34
+
35
+ 1. **Activity struct** holds interfaces as fields (one per external dependency)
36
+ 2. **Activity methods** are thin translation layers: validate inputs, call the interface, translate the output
37
+ 3. **Interfaces** are shaped by what activities need, informed by real SDK capabilities from the dependency map (see [dependency-resolution.md](./dependency-resolution.md))
38
+ 4. **Concrete implementations** of those interfaces contain the real SDK integration — client construction, request/response conversion, error handling
39
+
40
+ The skill generates all four pieces. Activity methods and interfaces are mechanical. Concrete implementations require SDK knowledge from the dependency resolution step.
41
+
42
+ ## Pitfalls
43
+
44
+ - **Context cancellation.** The activity's `context.Context` is a real stdlib context subject to cancellation. When Temporal times out or cancels an activity, the context is cancelled — but only if the activity calls `activity.RecordHeartbeat`. Without heartbeating, the activity goroutine never learns about cancellation and keeps running until it returns on its own
45
+ - Long-running activities must check `ctx.Done()` or `ctx.Err()` to respect cancellation:
46
+ ```go
47
+ select {
48
+ case <-ctx.Done():
49
+ return Result{}, ctx.Err()
50
+ default:
51
+ // continue processing
52
+ }
53
+ ```
54
+ - Activities that ignore `ctx.Done()` waste resources after timeout — the goroutine continues executing even though Temporal has already recorded the failure
@@ -0,0 +1,36 @@
1
+ # assignment
2
+
3
+ ## DSL
4
+
5
+ ```twf
6
+ paymentStatus = "pending"
7
+ status = "awaiting_payment"
8
+ ```
9
+
10
+ ## Go
11
+
12
+ ```go
13
+ paymentStatus := "pending"
14
+ status := "awaiting_payment"
15
+
16
+ // Reassignment (variable already declared):
17
+ status = "processing"
18
+
19
+ // Struct assignment from activity result or constructor:
20
+ order := Order{Status: "new", Items: items}
21
+
22
+ // Slice/map:
23
+ var results []ProcessResult
24
+ totals := make(map[string]int)
25
+ ```
26
+
27
+ ## Notes
28
+
29
+ - First use of a variable → `:=` (short declaration); subsequent assignments → `=`
30
+ - Workflow-scoped variables are declared at the top of the workflow function so signal/update handlers can access them via closure
31
+ - DSL assignments in `state:` blocks and signal handler bodies all map to the same workflow-level variables
32
+
33
+ ## Pitfalls
34
+
35
+ - **Closure scoping.** Variables declared inside a handler (signal, update, query) are invisible to the main workflow flow and to other handlers. If a value must be shared, declare the variable at workflow scope (top of the workflow function) and mutate it from the handler via closure
36
+ - **Determinism.** Workflow variables must not be assigned from non-deterministic sources (`time.Now()`, `rand`, HTTP responses, global mutable state). Use `workflow.Now()`, `workflow.SideEffect()`, or activity results instead. See [workflow-def.md](./workflow-def.md) for the full constraint list
@@ -0,0 +1,104 @@
1
+ # await all
2
+
3
+ ## DSL
4
+
5
+ ```twf
6
+ await all:
7
+ activity ReserveInventory(order) -> inventory
8
+ activity ChargePayment(order) -> payment
9
+ ```
10
+
11
+ ## Go
12
+
13
+ ```go
14
+ var inventory Inventory
15
+ var payment Payment
16
+ var inventoryErr, paymentErr error
17
+
18
+ wg := workflow.NewWaitGroup(ctx)
19
+
20
+ wg.Go(ctx, func(gCtx workflow.Context) {
21
+ inventoryErr = workflow.ExecuteActivity(gCtx, ReserveInventory, order).Get(gCtx, &inventory)
22
+ })
23
+ wg.Go(ctx, func(gCtx workflow.Context) {
24
+ paymentErr = workflow.ExecuteActivity(gCtx, ChargePayment, order).Get(gCtx, &payment)
25
+ })
26
+
27
+ wg.Wait(ctx)
28
+
29
+ if inventoryErr != nil {
30
+ return Result{}, inventoryErr
31
+ }
32
+ if paymentErr != nil {
33
+ return Result{}, paymentErr
34
+ }
35
+ ```
36
+
37
+ ## Fan-out pattern
38
+
39
+ ### DSL
40
+
41
+ ```twf
42
+ await all:
43
+ for (item in items):
44
+ activity ProcessBatchItem(item)
45
+ ```
46
+
47
+ ### Go
48
+
49
+ ```go
50
+ futures := make([]workflow.Future, len(items))
51
+ for i, item := range items {
52
+ futures[i] = workflow.ExecuteActivity(ctx, ProcessBatchItem, item)
53
+ }
54
+ for _, f := range futures {
55
+ if err := f.Get(ctx, nil); err != nil {
56
+ return Result{}, err
57
+ }
58
+ }
59
+ ```
60
+
61
+ ## Mixed activity + nexus
62
+
63
+ ### DSL
64
+
65
+ ```twf
66
+ await all:
67
+ activity ReserveInventory(order) -> inventory
68
+ nexus BillingEndpoint BillingService.ChargePayment(order.payment) -> payment
69
+ ```
70
+
71
+ ### Go
72
+
73
+ ```go
74
+ var inventory Inventory
75
+ var payment PaymentResult
76
+ var inventoryErr, paymentErr error
77
+
78
+ wg := workflow.NewWaitGroup(ctx)
79
+
80
+ wg.Go(ctx, func(gCtx workflow.Context) {
81
+ inventoryErr = workflow.ExecuteActivity(gCtx, ReserveInventory, order).Get(gCtx, &inventory)
82
+ })
83
+ wg.Go(ctx, func(gCtx workflow.Context) {
84
+ c := workflow.NewNexusClient("BillingEndpoint", "BillingService")
85
+ paymentErr = c.ExecuteOperation(gCtx, "ChargePayment", order.Payment, workflow.NexusOperationOptions{}).Get(gCtx, &payment)
86
+ })
87
+
88
+ wg.Wait(ctx)
89
+
90
+ if inventoryErr != nil {
91
+ return Result{}, inventoryErr
92
+ }
93
+ if paymentErr != nil {
94
+ return Result{}, paymentErr
95
+ }
96
+ ```
97
+
98
+ ## Notes
99
+
100
+ - Each statement in `await all:` runs in its own goroutine via `workflow.WaitGroup.Go`
101
+ - `wg.Wait(ctx)` blocks until all goroutines complete — no fragile completion predicates
102
+ - Fan-out with `for`: start all futures first, then `.Get` all — no goroutines needed since `ExecuteActivity` returns immediately
103
+ - Nexus operations in `await all:` follow the same goroutine pattern — `ExecuteOperation` returns a future, `.Get()` blocks in the goroutine
104
+ - Errors: check each goroutine's error after `wg.Wait`. For fail-fast, use `workflow.WithCancel` and cancel the context on first error
@@ -0,0 +1,193 @@
1
+ # await one
2
+
3
+ ## DSL
4
+
5
+ ```twf
6
+ await one:
7
+ signal PaymentReceived:
8
+ status = "processing"
9
+ timer(24h):
10
+ activity CancelOrder(orderId)
11
+ close fail(OrderResult{status: "cancelled"})
12
+ ```
13
+
14
+ ## History Hygiene
15
+
16
+ Per DSL semantics, non-winning cases are **not** cancelled by `await one` — they continue running until the workflow run ends (`close complete`, `close fail`, `close continue_as_new`, or external cancellation). The Go `Selector.Select` API mirrors this: it fires one case and returns, leaving all other registered futures and channels active.
17
+
18
+ Explicitly cancelling the losing timer is an **optimization** for history hygiene, not a correctness requirement. A non-cancelled timer will fire later and generate unnecessary workflow tasks.
19
+
20
+ ```go
21
+ // WASTEFUL: the losing timer fires later, adding unnecessary history events
22
+ sel.Select(ctx)
23
+ // timer still scheduled — generates a timer-fired event when it expires
24
+
25
+ // BETTER: cancel the timer in the winning handler to avoid wasted history events
26
+ sel.AddReceive(signalCh, func(ch workflow.ReceiveChannel, more bool) {
27
+ ch.Receive(ctx, nil)
28
+ cancelTimer() // prevents the timer from generating further history events
29
+ })
30
+ ```
31
+
32
+ ## Go
33
+
34
+ ```go
35
+ timerCtx, cancelTimer := workflow.WithCancel(ctx)
36
+
37
+ sel := workflow.NewSelector(ctx)
38
+
39
+ sel.AddReceive(paymentReceivedCh, func(ch workflow.ReceiveChannel, more bool) {
40
+ var sig PaymentReceivedSignal
41
+ ch.Receive(ctx, &sig)
42
+ status = "processing"
43
+ cancelTimer()
44
+ })
45
+
46
+ timerFuture := workflow.NewTimer(timerCtx, 24*time.Hour)
47
+ sel.AddFuture(timerFuture, func(f workflow.Future) {
48
+ if err := f.Get(ctx, nil); err != nil {
49
+ // timer cancelled — signal won the race
50
+ return
51
+ }
52
+ err := workflow.ExecuteActivity(ctx, CancelOrder, orderId).Get(ctx, nil)
53
+ if err != nil {
54
+ // handle activity error
55
+ }
56
+ // close fail handled after selector
57
+ })
58
+
59
+ sel.Select(ctx)
60
+ ```
61
+
62
+ ## Case types
63
+
64
+ **Signal case** — `sel.AddReceive(channel, handler)`
65
+ ```go
66
+ sel.AddReceive(signalCh, func(ch workflow.ReceiveChannel, more bool) {
67
+ var sig SignalType
68
+ ch.Receive(ctx, &sig)
69
+ // case body
70
+ })
71
+ ```
72
+
73
+ **Timer case** — `sel.AddFuture(workflow.NewTimer(...), handler)`
74
+ ```go
75
+ sel.AddFuture(workflow.NewTimer(ctx, duration), func(f workflow.Future) {
76
+ if err := f.Get(ctx, nil); err != nil {
77
+ // timer cancelled
78
+ return
79
+ }
80
+ // case body
81
+ })
82
+ ```
83
+
84
+ **Activity case** — `sel.AddFuture(workflow.ExecuteActivity(...), handler)`
85
+ ```go
86
+ sel.AddFuture(workflow.ExecuteActivity(ctx, DoWork, args), func(f workflow.Future) {
87
+ var result ResultType
88
+ if err := f.Get(ctx, &result); err != nil {
89
+ // handle error
90
+ return
91
+ }
92
+ // case body
93
+ })
94
+ ```
95
+
96
+ **Workflow case** — `sel.AddFuture(workflow.ExecuteChildWorkflow(...), handler)`
97
+ ```go
98
+ sel.AddFuture(workflow.ExecuteChildWorkflow(ctx, Child, args), func(f workflow.Future) {
99
+ var result ResultType
100
+ if err := f.Get(ctx, &result); err != nil {
101
+ // handle error
102
+ return
103
+ }
104
+ // case body
105
+ })
106
+ ```
107
+
108
+ **Nexus case** — `sel.AddFuture(client.ExecuteOperation(...), handler)`. Nexus operations return `NexusOperationFuture`, which satisfies `workflow.Future` — add to selector with `AddFuture`.
109
+ ```go
110
+ c := workflow.NewNexusClient("BillingEndpoint", "BillingService")
111
+ sel.AddFuture(c.ExecuteOperation(ctx, "ChargePayment", input, workflow.NexusOperationOptions{}), func(f workflow.Future) {
112
+ var result PaymentResult
113
+ if err := f.Get(ctx, &result); err != nil {
114
+ // handle error
115
+ return
116
+ }
117
+ // case body
118
+ })
119
+ ```
120
+
121
+ **Update case** — register handler separately (see [update-handler.md](./update-handler.md)), share state between the update handler and selector
122
+ ```go
123
+ // Update handlers are registered separately (see update-handler.md).
124
+ // To race an update against other events, share state between the update handler and selector:
125
+ sel.AddReceive(updateDoneCh, func(ch workflow.ReceiveChannel, more bool) {
126
+ ch.Receive(ctx, nil)
127
+ // react to update completion
128
+ })
129
+ ```
130
+
131
+ **Promise (ident) case** — add the existing future or channel to the selector
132
+ ```go
133
+ // future promise
134
+ sel.AddFuture(myFuture, func(f workflow.Future) {
135
+ var result ResultType
136
+ if err := f.Get(ctx, &result); err != nil {
137
+ // handle error
138
+ return
139
+ }
140
+ // case body
141
+ })
142
+
143
+ // condition promise — use a goroutine that signals a channel
144
+ condCh := workflow.NewChannel(ctx)
145
+ workflow.Go(ctx, func(gCtx workflow.Context) {
146
+ if err := workflow.Await(gCtx, func() bool { return myCondition }); err != nil {
147
+ return // context cancelled — don't send
148
+ }
149
+ condCh.Send(gCtx, true)
150
+ })
151
+ sel.AddReceive(condCh, func(ch workflow.ReceiveChannel, more bool) {
152
+ ch.Receive(ctx, nil)
153
+ // case body
154
+ })
155
+ ```
156
+
157
+ ## Notes
158
+
159
+ - `sel.Select(ctx)` blocks until exactly one case fires — the first to complete wins
160
+ - **Non-winning cases are not cancelled by `await one`.** Per DSL semantics, they continue running until the workflow run ends (`close complete`, `close fail`, `close continue_as_new`, or external cancellation). This matches `Selector.Select` Go SDK behavior. Explicit cancellation in the winning handler is an optimization for history hygiene, not a semantic requirement
161
+ - Cancelling a **timer** via `workflow.WithCancel` is clean — the timer stops and generates no further history events. Cancelling a **child workflow** sends a cancellation *request*; the child's actual behavior depends on its own cancellation handling. For controlling child lifecycle at parent completion, use `parent_close_policy` in child workflow options (see [options.md](./options.md))
162
+ - Uncancelled timers remain active and generate unnecessary workflow tasks when they fire
163
+ - Empty case bodies (just the colon in DSL) → handler function with only the `Receive`/`Get` call, no additional logic
164
+ - For `close` inside a case body: set a variable in the handler, check it after `sel.Select`, then return
165
+ - Nested `await all:` inside `await one:` → wrap the `await all` logic in a goroutine that signals a channel on completion, add as `AddReceive`:
166
+ ```go
167
+ allDoneCh := workflow.NewChannel(ctx)
168
+ workflow.Go(ctx, func(gCtx workflow.Context) {
169
+ wg := workflow.NewWaitGroup(gCtx)
170
+ wg.Go(gCtx, func(gCtx2 workflow.Context) { /* activity A */ })
171
+ wg.Go(gCtx, func(gCtx2 workflow.Context) { /* activity B */ })
172
+ wg.Wait(gCtx)
173
+ allDoneCh.Send(gCtx, true)
174
+ })
175
+ sel.AddReceive(allDoneCh, func(ch workflow.ReceiveChannel, more bool) {
176
+ ch.Receive(ctx, nil)
177
+ // all activities completed
178
+ })
179
+ ```
180
+
181
+ ## When to use: single Select vs loop
182
+
183
+ - **Single `sel.Select(ctx)`** (as shown above): use for one-time races — "whichever happens first wins" (e.g., payment or timeout)
184
+ - **`sel.Select(ctx)` in a loop**: use for event-driven workflows that process multiple events over time. Re-add cases each iteration or use persistent cases. Common for entity workflows that react to signals indefinitely
185
+ ```go
186
+ for {
187
+ sel := workflow.NewSelector(ctx)
188
+ sel.AddReceive(signalCh, func(ch workflow.ReceiveChannel, more bool) { ... })
189
+ sel.AddFuture(workflow.NewTimer(ctx, interval), func(f workflow.Future) { ... })
190
+ sel.Select(ctx)
191
+ if done { break }
192
+ }
193
+ ```
@@ -0,0 +1,35 @@
1
+ # await timer
2
+
3
+ ## DSL
4
+
5
+ ```twf
6
+ await timer(5m)
7
+ ```
8
+
9
+ ## Go
10
+
11
+ ```go
12
+ err := workflow.Sleep(ctx, 5*time.Minute)
13
+ if err != nil {
14
+ return Result{}, err
15
+ }
16
+ ```
17
+
18
+ ## Notes
19
+
20
+ - Duration units: `s` → `time.Second`, `m` → `time.Minute`, `h` → `time.Hour`, `d` → `24*time.Hour`
21
+ - Variable durations: `await timer(backoff)` → use the variable directly: `workflow.Sleep(ctx, backoff)`
22
+ - Inside `await one:`, timers use `workflow.NewTimer` instead — see [await-one.md](./await-one.md)
23
+
24
+ ## Pitfalls
25
+
26
+ - `workflow.Sleep` returns `*CanceledError` when the context is cancelled (either by `workflow.WithCancel` or by external workflow cancellation). Do not treat this as a generic failure — check for `*temporal.CanceledError` to enable clean cancellation handling:
27
+ ```go
28
+ err := workflow.Sleep(ctx, duration)
29
+ if err != nil {
30
+ if temporal.IsCanceledError(err) {
31
+ // clean cancellation — run cleanup logic
32
+ }
33
+ return Result{}, err
34
+ }
35
+ ```
@@ -0,0 +1,71 @@
1
+ # close
2
+
3
+ ## close complete
4
+
5
+ ### DSL
6
+
7
+ ```twf
8
+ close complete(Result{status: "done"})
9
+ ```
10
+
11
+ ### Go
12
+
13
+ ```go
14
+ return Result{Status: "done"}, nil
15
+ ```
16
+
17
+ ## close complete (no value)
18
+
19
+ ### DSL
20
+
21
+ ```twf
22
+ close complete
23
+ ```
24
+
25
+ ### Go
26
+
27
+ ```go
28
+ return nil
29
+ ```
30
+
31
+ ## close fail
32
+
33
+ ### DSL
34
+
35
+ ```twf
36
+ close fail(OrderResult{status: "cancelled"})
37
+ ```
38
+
39
+ ### Go
40
+
41
+ ```go
42
+ return OrderResult{}, temporal.NewApplicationError("order cancelled", "OrderCancelled", OrderResult{Status: "cancelled"})
43
+ ```
44
+
45
+ ## close continue_as_new
46
+
47
+ ### DSL
48
+
49
+ ```twf
50
+ close continue_as_new(userId, user)
51
+ ```
52
+
53
+ ### Go
54
+
55
+ ```go
56
+ return workflow.NewContinueAsNewError(ctx, UserEntity, userId, user)
57
+ ```
58
+
59
+ ## Notes
60
+
61
+ - `close complete(value)` → `return value, nil`
62
+ - `close fail(value)` → return zero value + `temporal.NewApplicationError`. Pass the struct as error details to preserve structured data across workflow boundaries. Use `NewNonRetryableApplicationError` when the failure should not be retried
63
+ - `close continue_as_new` passes args to the same workflow function via `workflow.NewContinueAsNewError`
64
+ - `close complete` with no args and no return type → `return nil`
65
+
66
+ ## When to use each error type
67
+
68
+ - **`temporal.NewApplicationError(msg, errType, details...)`** — structured workflow error. The `errType` string enables callers to distinguish error types. Pass the original struct as `details` to preserve data across workflow boundaries. Retryable by default
69
+ - **`temporal.NewNonRetryableApplicationError(msg, errType, cause, details...)`** — same as above but the workflow will not be retried. Use for permanent failures (invalid input, business rule violations)
70
+ - **Retryable vs non-retryable:** retryable errors trigger the workflow's retry policy (if configured). Non-retryable errors fail the workflow immediately regardless of retry policy
71
+ - Plain `fmt.Errorf` or `errors.New` produces an untyped error — callers cannot distinguish error types or extract structured details. Prefer `ApplicationError` for workflow-boundary errors
@@ -0,0 +1,176 @@
1
+ # composite patterns
2
+
3
+ Patterns in isolation are straightforward. Bugs cluster where patterns combine. These worked examples show the most common compositions and their pitfalls.
4
+
5
+ ## Pattern 1: Update handler + condition + selector (HumanReview)
6
+
7
+ An update handler sets a condition that races against a timer in a selector: "approve within the deadline, or auto-escalate."
8
+
9
+ ### DSL
10
+
11
+ ```twf
12
+ workflow HumanReview(docId: string) -> (ReviewResult):
13
+ state:
14
+ condition reviewComplete
15
+
16
+ update SubmitReview(decision: string) -> (ReviewResult):
17
+ result = decision
18
+ set reviewComplete
19
+ return ReviewResult{decision: result}
20
+
21
+ await one:
22
+ reviewComplete:
23
+ close complete(ReviewResult{decision: result})
24
+ timer(48h):
25
+ activity Escalate(docId)
26
+ close complete(ReviewResult{decision: "escalated"})
27
+ ```
28
+
29
+ ### Go
30
+
31
+ ```go
32
+ func HumanReview(ctx workflow.Context, docId string) (ReviewResult, error) {
33
+ reviewComplete := false
34
+ var result string
35
+
36
+ // Register update handler FIRST — before any blocking call
37
+ err := workflow.SetUpdateHandlerWithOptions(ctx, "SubmitReview",
38
+ func(ctx workflow.Context, decision string) (ReviewResult, error) {
39
+ result = decision
40
+ reviewComplete = true
41
+ return ReviewResult{Decision: result}, nil
42
+ },
43
+ workflow.UpdateHandlerOptions{},
44
+ )
45
+ if err != nil {
46
+ return ReviewResult{}, err
47
+ }
48
+
49
+ // Race: condition (set by update) vs timer
50
+ timerCtx, cancelTimer := workflow.WithCancel(ctx)
51
+
52
+ sel := workflow.NewSelector(ctx)
53
+
54
+ // Condition case — wrap in goroutine + channel (conditions aren't futures)
55
+ condCh := workflow.NewChannel(ctx)
56
+ workflow.Go(ctx, func(gCtx workflow.Context) {
57
+ if err := workflow.Await(gCtx, func() bool { return reviewComplete }); err != nil {
58
+ return // context cancelled
59
+ }
60
+ condCh.Send(gCtx, true)
61
+ })
62
+ sel.AddReceive(condCh, func(ch workflow.ReceiveChannel, more bool) {
63
+ ch.Receive(ctx, nil)
64
+ cancelTimer()
65
+ })
66
+
67
+ // Timer case
68
+ sel.AddFuture(workflow.NewTimer(timerCtx, 48*time.Hour), func(f workflow.Future) {
69
+ if err := f.Get(ctx, nil); err != nil {
70
+ return // timer cancelled — review won the race
71
+ }
72
+ var a *Activities
73
+ _ = workflow.ExecuteActivity(ctx, a.Escalate, docId).Get(ctx, nil)
74
+ result = "escalated"
75
+ })
76
+
77
+ sel.Select(ctx)
78
+
79
+ // Wait for any in-flight update handlers before returning
80
+ _ = workflow.Await(ctx, func() bool { return workflow.AllHandlersFinished(ctx) })
81
+
82
+ return ReviewResult{Decision: result}, nil
83
+ }
84
+ ```
85
+
86
+ ### Why this is tricky
87
+
88
+ 1. **Update handler must be registered before `sel.Select`** — if the selector blocks first, updates arriving during the race window are rejected
89
+ 2. **Condition is not a future** — it must be bridged to the selector via a goroutine + channel (see [await-one.md](./await-one.md), "condition promise" pattern)
90
+ 3. **Timer should be cancelled in the condition handler** — per DSL semantics, non-winning cases are not automatically cancelled; the timer continues running until workflow completion. Cancelling it in the winning handler is an optimization that prevents unnecessary history events (a timer-fired event 48h later)
91
+ 4. **`AllHandlersFinished` wait before return** — without this, an in-flight update handler is abandoned when the workflow completes (see [update-handler.md](./update-handler.md))
92
+
93
+ ---
94
+
95
+ ## Pattern 2: Signal handler goroutine + main-thread await after sleep (ManageRetention)
96
+
97
+ A long-running workflow sleeps for a retention period while a signal handler goroutine listens for early cancellation.
98
+
99
+ ### DSL
100
+
101
+ ```twf
102
+ workflow ManageRetention(docId: string, retentionDays: int):
103
+ state:
104
+ condition cancelled
105
+
106
+ signal CancelRetention():
107
+ set cancelled
108
+
109
+ await one:
110
+ cancelled:
111
+ activity DeleteDocument(docId)
112
+ close complete
113
+ timer(retentionDays * 24h):
114
+ activity DeleteDocument(docId)
115
+ close complete
116
+ ```
117
+
118
+ ### Go
119
+
120
+ ```go
121
+ func ManageRetention(ctx workflow.Context, docId string, retentionDays int) error {
122
+ cancelled := false
123
+
124
+ // Signal channel + handler goroutine
125
+ cancelRetentionCh := workflow.GetSignalChannel(ctx, "CancelRetention")
126
+ workflow.Go(ctx, func(gCtx workflow.Context) {
127
+ for {
128
+ cancelRetentionCh.Receive(gCtx, nil)
129
+ cancelled = true
130
+ }
131
+ })
132
+
133
+ // Race: signal-set condition vs retention timer
134
+ timerCtx, cancelTimer := workflow.WithCancel(ctx)
135
+
136
+ sel := workflow.NewSelector(ctx)
137
+
138
+ // Condition case
139
+ condCh := workflow.NewChannel(ctx)
140
+ workflow.Go(ctx, func(gCtx workflow.Context) {
141
+ if err := workflow.Await(gCtx, func() bool { return cancelled }); err != nil {
142
+ return
143
+ }
144
+ condCh.Send(gCtx, true)
145
+ })
146
+ sel.AddReceive(condCh, func(ch workflow.ReceiveChannel, more bool) {
147
+ ch.Receive(ctx, nil)
148
+ cancelTimer()
149
+ })
150
+
151
+ // Timer case
152
+ duration := time.Duration(retentionDays) * 24 * time.Hour
153
+ sel.AddFuture(workflow.NewTimer(timerCtx, duration), func(f workflow.Future) {
154
+ if err := f.Get(ctx, nil); err != nil {
155
+ return // timer cancelled — signal won
156
+ }
157
+ })
158
+
159
+ sel.Select(ctx)
160
+
161
+ // Both paths delete the document
162
+ var a *Activities
163
+ err := workflow.ExecuteActivity(ctx, a.DeleteDocument, docId).Get(ctx, nil)
164
+ if err != nil {
165
+ return err
166
+ }
167
+ return nil
168
+ }
169
+ ```
170
+
171
+ ### Why this is tricky
172
+
173
+ 1. **Signal handler goroutine runs independently of the selector** — it loops forever, processing every signal arrival. The condition bridge (`workflow.Await` + channel send) connects it to the selector
174
+ 2. **The signal handler mutates `cancelled`, the selector reads it via the condition goroutine** — two goroutines share state through a bool, which is safe in Temporal's cooperative scheduling model but would be a data race in normal Go
175
+ 3. **Timer cancellation is recommended** — per DSL semantics, non-winning cases are not automatically cancelled; the timer continues until workflow completion regardless. Cancelling it in the winning handler avoids a wasted timer-fired history event
176
+ 4. **Shared post-race logic** — when both branches do the same thing, factor the common activity call after `sel.Select` instead of duplicating inside handlers