@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.
- package/LICENSE +21 -0
- package/README.md +38 -0
- package/package.json +37 -0
- package/skills/MANIFEST.json +373 -0
- package/skills/MANIFEST.md +121 -0
- package/skills/temporal-architect/SKILL.md +99 -0
- package/skills/temporal-architect/reference/decomposition.md +78 -0
- package/skills/temporal-architect-author-go/README.md +16 -0
- package/skills/temporal-architect-author-go/SKILL.md +191 -0
- package/skills/temporal-architect-author-go/SUBAGENT_ADOPTION.md +161 -0
- package/skills/temporal-architect-author-go/reference/activity-call.md +73 -0
- package/skills/temporal-architect-author-go/reference/activity-def.md +54 -0
- package/skills/temporal-architect-author-go/reference/assignment.md +36 -0
- package/skills/temporal-architect-author-go/reference/await-all.md +104 -0
- package/skills/temporal-architect-author-go/reference/await-one.md +193 -0
- package/skills/temporal-architect-author-go/reference/await-timer.md +35 -0
- package/skills/temporal-architect-author-go/reference/close.md +71 -0
- package/skills/temporal-architect-author-go/reference/composite-patterns.md +176 -0
- package/skills/temporal-architect-author-go/reference/condition.md +56 -0
- package/skills/temporal-architect-author-go/reference/control-flow.md +151 -0
- package/skills/temporal-architect-author-go/reference/dependency-resolution.md +29 -0
- package/skills/temporal-architect-author-go/reference/detach.md +52 -0
- package/skills/temporal-architect-author-go/reference/heartbeat.md +84 -0
- package/skills/temporal-architect-author-go/reference/nexus-service-def.md +73 -0
- package/skills/temporal-architect-author-go/reference/nexus.md +35 -0
- package/skills/temporal-architect-author-go/reference/options.md +138 -0
- package/skills/temporal-architect-author-go/reference/promise.md +73 -0
- package/skills/temporal-architect-author-go/reference/proto-driven.md +197 -0
- package/skills/temporal-architect-author-go/reference/query-handler.md +34 -0
- package/skills/temporal-architect-author-go/reference/signal-handler.md +73 -0
- package/skills/temporal-architect-author-go/reference/three-layer-testing.md +173 -0
- package/skills/temporal-architect-author-go/reference/types.md +72 -0
- package/skills/temporal-architect-author-go/reference/update-handler.md +64 -0
- package/skills/temporal-architect-author-go/reference/worker.md +215 -0
- package/skills/temporal-architect-author-go/reference/workflow-call.md +37 -0
- package/skills/temporal-architect-author-go/reference/workflow-def.md +45 -0
- package/skills/temporal-architect-author-infra/README.md +16 -0
- package/skills/temporal-architect-author-infra/SKILL.md +132 -0
- package/skills/temporal-architect-author-infra/reference/tcld.md +112 -0
- package/skills/temporal-architect-author-infra/reference/terraform.md +125 -0
- package/skills/temporal-architect-design/README.md +16 -0
- package/skills/temporal-architect-design/SKILL.md +224 -0
- package/skills/temporal-architect-design/reference/LANGUAGE.md +5 -0
- package/skills/temporal-architect-design/reference/anti-patterns.md +332 -0
- package/skills/temporal-architect-design/reference/common-errors.md +88 -0
- package/skills/temporal-architect-design/reference/core-principles.md +52 -0
- package/skills/temporal-architect-design/reference/design-checklist.md +59 -0
- package/skills/temporal-architect-design/reference/namespaces.md +84 -0
- package/skills/temporal-architect-design/reference/notation-examples.md +304 -0
- package/skills/temporal-architect-design/reference/notation-reference.md +70 -0
- package/skills/temporal-architect-design/reference/primitives-reference.md +65 -0
- package/skills/temporal-architect-design/reference/project-discovery-subagent.md +80 -0
- package/skills/temporal-architect-design/reference/reverse-engineering.md +53 -0
- package/skills/temporal-architect-design/reference/twf-conventions.md +43 -0
- package/skills/temporal-architect-design/reference/workflow-boundaries.md +43 -0
- package/skills/temporal-architect-design/topics/activities-advanced.md +358 -0
- package/skills/temporal-architect-design/topics/activities-advanced.twf +107 -0
- package/skills/temporal-architect-design/topics/child-workflows.md +347 -0
- package/skills/temporal-architect-design/topics/child-workflows.twf +171 -0
- package/skills/temporal-architect-design/topics/long-running.md +230 -0
- package/skills/temporal-architect-design/topics/long-running.twf +100 -0
- package/skills/temporal-architect-design/topics/nexus.md +248 -0
- package/skills/temporal-architect-design/topics/nexus.twf +148 -0
- package/skills/temporal-architect-design/topics/patterns.md +469 -0
- package/skills/temporal-architect-design/topics/patterns.twf +346 -0
- package/skills/temporal-architect-design/topics/promises-conditions.md +179 -0
- package/skills/temporal-architect-design/topics/promises-conditions.twf +213 -0
- package/skills/temporal-architect-design/topics/signals-queries-updates.md +319 -0
- package/skills/temporal-architect-design/topics/signals-queries-updates.twf +234 -0
- package/skills/temporal-architect-design/topics/task-queues.md +205 -0
- package/skills/temporal-architect-design/topics/task-queues.twf +184 -0
- package/skills/temporal-architect-design/topics/testing.md +437 -0
- package/skills/temporal-architect-design/topics/testing.twf +177 -0
- package/skills/temporal-architect-design/topics/timers-scheduling.md +131 -0
- package/skills/temporal-architect-design/topics/timers-scheduling.twf +129 -0
- package/skills/temporal-architect-design/topics/versioning.md +434 -0
- 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
|