@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,56 @@
|
|
|
1
|
+
# condition
|
|
2
|
+
|
|
3
|
+
## DSL
|
|
4
|
+
|
|
5
|
+
```twf
|
|
6
|
+
workflow JobCoordinator(config: JobConfig):
|
|
7
|
+
state:
|
|
8
|
+
condition jobReady
|
|
9
|
+
|
|
10
|
+
# ... later in body:
|
|
11
|
+
set jobReady
|
|
12
|
+
|
|
13
|
+
# ... elsewhere:
|
|
14
|
+
await jobReady
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Go
|
|
18
|
+
|
|
19
|
+
```go
|
|
20
|
+
func JobCoordinator(ctx workflow.Context, config JobConfig) error {
|
|
21
|
+
jobReady := false
|
|
22
|
+
|
|
23
|
+
// ... later in body:
|
|
24
|
+
jobReady = true
|
|
25
|
+
|
|
26
|
+
// ... elsewhere:
|
|
27
|
+
err := workflow.Await(ctx, func() bool { return jobReady })
|
|
28
|
+
if err != nil {
|
|
29
|
+
return err
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Notes
|
|
35
|
+
|
|
36
|
+
- `condition name` in `state:` → `name := false` (a `bool` variable)
|
|
37
|
+
- `set name` → `name = true`
|
|
38
|
+
- `unset name` → `name = false`
|
|
39
|
+
- `await name` → `workflow.Await(ctx, func() bool { return name })`
|
|
40
|
+
- Conditions in `await one:` become selector cases via a goroutine that awaits the condition and sends to a channel — see the "condition promise" pattern in [await-one.md](./await-one.md)
|
|
41
|
+
|
|
42
|
+
## Pitfalls
|
|
43
|
+
|
|
44
|
+
- **Never poll with `workflow.Sleep` in a loop.** Each `Sleep` generates 2 history events (timer-started + timer-fired). A tight polling loop can exhaust the 51,200 event limit and force the workflow into a non-recoverable state. Always use `workflow.Await` — it generates zero history events and re-evaluates on every state transition:
|
|
45
|
+
```go
|
|
46
|
+
// WRONG: polling loop — 2 history events per iteration
|
|
47
|
+
for !ready {
|
|
48
|
+
workflow.Sleep(ctx, 5*time.Second)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// RIGHT: zero history events, wakes on any state change
|
|
52
|
+
workflow.Await(ctx, func() bool { return ready })
|
|
53
|
+
```
|
|
54
|
+
- **Indefinite blocking.** `workflow.Await` blocks until the condition returns `true`. If the condition is never set (logic bug, missing signal handler, unreachable code path), the workflow hangs indefinitely. Consider using `workflow.AwaitWithTimeout` when a bounded wait is appropriate
|
|
55
|
+
- **Time-based conditions are unreliable.** `workflow.Await(ctx, func() bool { return workflow.Now(ctx).After(deadline) })` may never return — the condition is only re-evaluated on workflow state transitions (signals, activity completions, etc.), not on wall-clock time. Use `workflow.Sleep` or `workflow.NewTimer` for time-based waits
|
|
56
|
+
- `workflow.Await` returns `*CanceledError` if the context is cancelled
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# control flow
|
|
2
|
+
|
|
3
|
+
## if / else
|
|
4
|
+
|
|
5
|
+
### DSL
|
|
6
|
+
|
|
7
|
+
```twf
|
|
8
|
+
if (validated.priority == "high"):
|
|
9
|
+
activity ExpediteOrder(order)
|
|
10
|
+
else:
|
|
11
|
+
activity StandardProcessing(order)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
### Go
|
|
15
|
+
|
|
16
|
+
```go
|
|
17
|
+
if validated.Priority == "high" {
|
|
18
|
+
err := workflow.ExecuteActivity(ctx, ExpediteOrder, order).Get(ctx, nil)
|
|
19
|
+
if err != nil {
|
|
20
|
+
return Result{}, err
|
|
21
|
+
}
|
|
22
|
+
} else {
|
|
23
|
+
err := workflow.ExecuteActivity(ctx, StandardProcessing, order).Get(ctx, nil)
|
|
24
|
+
if err != nil {
|
|
25
|
+
return Result{}, err
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## for (iteration)
|
|
31
|
+
|
|
32
|
+
### DSL
|
|
33
|
+
|
|
34
|
+
```twf
|
|
35
|
+
for (item in order.items):
|
|
36
|
+
activity ProcessItem(item)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Go
|
|
40
|
+
|
|
41
|
+
```go
|
|
42
|
+
for _, item := range order.Items {
|
|
43
|
+
err := workflow.ExecuteActivity(ctx, ProcessItem, item).Get(ctx, nil)
|
|
44
|
+
if err != nil {
|
|
45
|
+
return Result{}, err
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## for (conditional)
|
|
51
|
+
|
|
52
|
+
### DSL
|
|
53
|
+
|
|
54
|
+
```twf
|
|
55
|
+
for (retries < maxRetries):
|
|
56
|
+
activity Attempt(data)
|
|
57
|
+
retries = retries + 1
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Go
|
|
61
|
+
|
|
62
|
+
```go
|
|
63
|
+
for retries < maxRetries {
|
|
64
|
+
err := workflow.ExecuteActivity(ctx, Attempt, data).Get(ctx, nil)
|
|
65
|
+
if err != nil {
|
|
66
|
+
return Result{}, err
|
|
67
|
+
}
|
|
68
|
+
retries++
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## for (infinite loop)
|
|
73
|
+
|
|
74
|
+
### DSL
|
|
75
|
+
|
|
76
|
+
```twf
|
|
77
|
+
for:
|
|
78
|
+
# body
|
|
79
|
+
if (done):
|
|
80
|
+
break
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Go
|
|
84
|
+
|
|
85
|
+
```go
|
|
86
|
+
for {
|
|
87
|
+
// body
|
|
88
|
+
if done {
|
|
89
|
+
break
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## switch
|
|
95
|
+
|
|
96
|
+
### DSL
|
|
97
|
+
|
|
98
|
+
```twf
|
|
99
|
+
switch (phase):
|
|
100
|
+
case "draft":
|
|
101
|
+
# ...
|
|
102
|
+
case "approved":
|
|
103
|
+
# ...
|
|
104
|
+
else:
|
|
105
|
+
# ...
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Go
|
|
109
|
+
|
|
110
|
+
```go
|
|
111
|
+
switch phase {
|
|
112
|
+
case "draft":
|
|
113
|
+
// ...
|
|
114
|
+
case "approved":
|
|
115
|
+
// ...
|
|
116
|
+
default:
|
|
117
|
+
// ...
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Notes
|
|
122
|
+
|
|
123
|
+
- `break` → `break`, `continue` → `continue` — direct mapping
|
|
124
|
+
- DSL `else:` in switch → Go `default:`
|
|
125
|
+
- DSL boolean operators: `and` → `&&`, `or` → `||`, `not` → `!`
|
|
126
|
+
- All condition expressions (`if`, `for`, `switch`) must be deterministic — no `time.Now()`, `rand`, or non-deterministic function calls. See [workflow-def.md](./workflow-def.md) for the full constraint list
|
|
127
|
+
|
|
128
|
+
## When to use: sequential vs parallel iteration
|
|
129
|
+
|
|
130
|
+
- **Sequential** (`.Get()` inside loop): each iteration waits for the previous to complete. Use when order matters or when items depend on prior results
|
|
131
|
+
```go
|
|
132
|
+
for _, item := range items {
|
|
133
|
+
err := workflow.ExecuteActivity(ctx, Process, item).Get(ctx, nil)
|
|
134
|
+
// blocks here — next iteration starts after this one completes
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
- **Parallel** (collect futures, `.Get()` after loop): all iterations start concurrently. Use when items are independent
|
|
138
|
+
```go
|
|
139
|
+
var futures []workflow.Future
|
|
140
|
+
for _, item := range items {
|
|
141
|
+
futures = append(futures, workflow.ExecuteActivity(ctx, Process, item))
|
|
142
|
+
}
|
|
143
|
+
for _, f := range futures {
|
|
144
|
+
if err := f.Get(ctx, nil); err != nil { ... }
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
- The DSL does not distinguish these — infer from context. If unsure, ask the user
|
|
148
|
+
|
|
149
|
+
## Pitfalls
|
|
150
|
+
|
|
151
|
+
- **Manual retry loops** (the `for (retries < max)` pattern) are almost always inferior to SDK `RetryPolicy` on `ActivityOptions`. Use `RetryPolicy` for standard retry logic; reserve manual loops for non-standard control flow (e.g., retry with modified input, conditional retry based on error type)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# dependency resolution
|
|
2
|
+
|
|
3
|
+
Scan all activities in the `.twf` and identify external integration points. Most activities are thin wrappers around calls to external systems — the activity itself is simple, but the dependency behind it is not.
|
|
4
|
+
|
|
5
|
+
## For each activity
|
|
6
|
+
|
|
7
|
+
- **Categorize:** external API call, storage operation, protocol client, or pure logic
|
|
8
|
+
- **Check existing code:** does the project already have a client/library for this?
|
|
9
|
+
- **Check `go.mod`:** is a relevant SDK already imported?
|
|
10
|
+
- **If unresolved:** suggest specific options with tradeoffs to the user
|
|
11
|
+
- **Read the chosen dependency's API:** identify the method the activity will call, then trace its signature to concrete types. See [types.md](./types.md) for the full resolution strategy
|
|
12
|
+
|
|
13
|
+
Resolve as many as possible early — it prevents expensive rework later. If a dependency choice is unclear or blocked on another decision, defer it and continue.
|
|
14
|
+
|
|
15
|
+
## Deliverable
|
|
16
|
+
|
|
17
|
+
A dependency map, presented to the user for confirmation before generation begins.
|
|
18
|
+
|
|
19
|
+
A dependency is resolved when you can write the call expression with verified types. The map should include the method, every parameter type, and the return type — all confirmed from `go doc` or source, not inferred from names.
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
Example dependency map:
|
|
23
|
+
ChargePayment → stripe-go
|
|
24
|
+
paymentintent.New(params *stripe.PaymentIntentParams) (*stripe.PaymentIntent, error)
|
|
25
|
+
SendPaymentConfirmation → sendgrid-go
|
|
26
|
+
client.SendWithContext(ctx, mail *sgmail.SGMailV3) (*rest.Response, error)
|
|
27
|
+
LoadOrderRecord → database/sql (no external dependency)
|
|
28
|
+
CalculateTotal → pure logic (no dependency)
|
|
29
|
+
```
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# detach
|
|
2
|
+
|
|
3
|
+
## DSL
|
|
4
|
+
|
|
5
|
+
```twf
|
|
6
|
+
detach workflow NotifyCustomer(order.customer)
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Go
|
|
10
|
+
|
|
11
|
+
```go
|
|
12
|
+
childCtx := workflow.WithChildOptions(ctx, workflow.ChildWorkflowOptions{
|
|
13
|
+
ParentClosePolicy: enumspb.PARENT_CLOSE_POLICY_ABANDON,
|
|
14
|
+
})
|
|
15
|
+
childFuture := workflow.ExecuteChildWorkflow(childCtx, NotifyCustomer, order.Customer)
|
|
16
|
+
// DO NOT skip this — child may silently fail to spawn if parent completes first
|
|
17
|
+
if err := childFuture.GetChildWorkflowExecution().Get(ctx, nil); err != nil {
|
|
18
|
+
return Result{}, err
|
|
19
|
+
}
|
|
20
|
+
// Do NOT call childFuture.Get() — that waits for completion, defeating detach
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Detach with nexus
|
|
24
|
+
|
|
25
|
+
### DSL
|
|
26
|
+
|
|
27
|
+
```twf
|
|
28
|
+
detach nexus NotificationsEndpoint NotificationsService.SendConfirmation(order.customer, paymentResult)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Go
|
|
32
|
+
|
|
33
|
+
```go
|
|
34
|
+
c := workflow.NewNexusClient("NotificationsEndpoint", "NotificationsService")
|
|
35
|
+
c.ExecuteOperation(ctx, "SendConfirmation", sendConfirmationInput, workflow.NexusOperationOptions{})
|
|
36
|
+
// No .Get() — fire-and-forget. The operation starts when the schedule command is processed
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Notes
|
|
40
|
+
|
|
41
|
+
- `detach` = start the child but never wait for its result
|
|
42
|
+
- Set `ParentClosePolicy` to `ABANDON` so the child survives if the parent completes
|
|
43
|
+
- Always call `GetChildWorkflowExecution().Get()` to confirm the child started — without this, the child may never spawn if the parent completes first
|
|
44
|
+
- Do not call `.Get()` on the child workflow future itself (that would wait for completion)
|
|
45
|
+
- The child workflow runs independently and its success/failure does not affect the parent
|
|
46
|
+
- **Nexus fire-and-forget caveat:** omitting `.Get()` is an emergent pattern, not a first-class SDK mode. The caller workflow must not complete before the `ScheduleNexusOperation` command is processed, or the operation may not start. When the handler workflow completes, it attempts a callback to the (already-completed) caller, producing an ignorable error
|
|
47
|
+
|
|
48
|
+
## When to use
|
|
49
|
+
|
|
50
|
+
- Use `detach` when the parent does not need the child's result and the child should outlive the parent (e.g., sending a notification after order completion)
|
|
51
|
+
- Use a normal child workflow call (with `.Get()`) when the parent needs the result or should fail if the child fails
|
|
52
|
+
- `detach` uses `ABANDON` so the child survives parent completion independently. The default `ParentClosePolicy` is `TERMINATE` (child killed). `REQUEST_CANCEL` is a middle ground — the child receives a cancellation request and can handle it gracefully before stopping, but does not run indefinitely like `ABANDON`
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# heartbeat
|
|
2
|
+
|
|
3
|
+
## DSL
|
|
4
|
+
|
|
5
|
+
```twf
|
|
6
|
+
activity ProcessLargeFile(fileId: string) -> (ProcessResult):
|
|
7
|
+
file = download(fileId)
|
|
8
|
+
for (chunk in file.chunks):
|
|
9
|
+
process(chunk)
|
|
10
|
+
heartbeat(progress: {current: chunk, total: len(file.chunks)})
|
|
11
|
+
return ProcessResult{success: true}
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Calling the activity with heartbeat and timeout options:
|
|
15
|
+
|
|
16
|
+
```twf
|
|
17
|
+
workflow ProcessFiles(fileId: string) -> (ProcessResult):
|
|
18
|
+
activity ProcessLargeFile(fileId) -> result
|
|
19
|
+
options:
|
|
20
|
+
start_to_close_timeout: 2h
|
|
21
|
+
heartbeat_timeout: 30s
|
|
22
|
+
close complete(result)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Go
|
|
26
|
+
|
|
27
|
+
```go
|
|
28
|
+
func ProcessLargeFile(ctx context.Context, fileId string) (ProcessResult, error) {
|
|
29
|
+
file, err := download(ctx, fileId)
|
|
30
|
+
if err != nil {
|
|
31
|
+
return ProcessResult{}, err
|
|
32
|
+
}
|
|
33
|
+
for _, chunk := range file.Chunks {
|
|
34
|
+
if err := process(ctx, chunk); err != nil {
|
|
35
|
+
return ProcessResult{}, err
|
|
36
|
+
}
|
|
37
|
+
activity.RecordHeartbeat(ctx, map[string]interface{}{
|
|
38
|
+
"current": chunk,
|
|
39
|
+
"total": len(file.Chunks),
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
return ProcessResult{Success: true}, nil
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Notes
|
|
47
|
+
|
|
48
|
+
- `heartbeat(args)` → `activity.RecordHeartbeat(ctx, details...)` — activity-only, never in workflows
|
|
49
|
+
- Heartbeat details are arbitrary; use a struct or map
|
|
50
|
+
- Set `HeartbeatTimeout` in `ActivityOptions` on the calling side — see [options.md](./options.md)
|
|
51
|
+
- The SDK throttles heartbeats automatically (sent at `heartbeatTimeout * 0.8`, capped at 60s). No manual batching needed — call `RecordHeartbeat` as often as desired
|
|
52
|
+
- The plain function shown above becomes a struct method in practice — see [activity-def.md](./activity-def.md) notes
|
|
53
|
+
|
|
54
|
+
## Resume pattern
|
|
55
|
+
|
|
56
|
+
Activities that heartbeat should resume from the last recorded progress on retry. Check for previous heartbeat details at activity start:
|
|
57
|
+
|
|
58
|
+
```go
|
|
59
|
+
func (a *Activities) ProcessLargeFile(ctx context.Context, fileId string) (ProcessResult, error) {
|
|
60
|
+
startIdx := 0
|
|
61
|
+
if activity.HasHeartbeatDetails(ctx) {
|
|
62
|
+
var lastIdx int
|
|
63
|
+
if err := activity.GetHeartbeatDetails(ctx, &lastIdx); err == nil {
|
|
64
|
+
startIdx = lastIdx + 1
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
file, err := a.download(ctx, fileId)
|
|
69
|
+
if err != nil {
|
|
70
|
+
return ProcessResult{}, err
|
|
71
|
+
}
|
|
72
|
+
for i := startIdx; i < len(file.Chunks); i++ {
|
|
73
|
+
if err := a.process(ctx, file.Chunks[i]); err != nil {
|
|
74
|
+
return ProcessResult{}, err
|
|
75
|
+
}
|
|
76
|
+
activity.RecordHeartbeat(ctx, i)
|
|
77
|
+
}
|
|
78
|
+
return ProcessResult{Success: true}, nil
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
- `HasHeartbeatDetails` returns `false` on the first attempt (no prior failure)
|
|
83
|
+
- Details come from the last heartbeat delivered to the server (post-throttling) — may be slightly behind the last `RecordHeartbeat` call
|
|
84
|
+
- Heartbeat recording without a resume pattern is incomplete — the activity restarts from the beginning on every retry, wasting the progress tracking
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# nexus service definition
|
|
2
|
+
|
|
3
|
+
## DSL
|
|
4
|
+
|
|
5
|
+
```twf
|
|
6
|
+
nexus service BillingService:
|
|
7
|
+
operation ChargePayment(PaymentRequest) -> (PaymentResult)
|
|
8
|
+
operation RefundPayment(RefundRequest) -> (RefundResult)
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Go — Service contract (shared types)
|
|
12
|
+
|
|
13
|
+
```go
|
|
14
|
+
const BillingServiceName = "BillingService"
|
|
15
|
+
const ChargePaymentOp = "ChargePayment"
|
|
16
|
+
const RefundPaymentOp = "RefundPayment"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Go — Async operation handler (workflow-backed)
|
|
20
|
+
|
|
21
|
+
```go
|
|
22
|
+
var ChargePaymentOperation = temporalnexus.NewWorkflowRunOperation(
|
|
23
|
+
ChargePaymentOp,
|
|
24
|
+
BillingChargeWorkflow,
|
|
25
|
+
func(ctx context.Context, input PaymentRequest, options nexus.StartOperationOptions) (client.StartWorkflowOptions, error) {
|
|
26
|
+
return client.StartWorkflowOptions{
|
|
27
|
+
ID: "payment-" + input.OrderID, // business-meaningful ID for deduplication
|
|
28
|
+
}, nil
|
|
29
|
+
},
|
|
30
|
+
)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Go — Sync operation handler
|
|
34
|
+
|
|
35
|
+
```go
|
|
36
|
+
var RefundPaymentOperation = nexus.NewSyncOperation(RefundPaymentOp, func(ctx context.Context, input RefundRequest, options nexus.StartOperationOptions) (RefundResult, error) {
|
|
37
|
+
// Direct implementation or use temporalnexus.GetClient(ctx) for Temporal client calls
|
|
38
|
+
return RefundResult{}, nil
|
|
39
|
+
})
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Go — Registration on worker
|
|
43
|
+
|
|
44
|
+
```go
|
|
45
|
+
service := nexus.NewService(BillingServiceName)
|
|
46
|
+
err := service.Register(ChargePaymentOperation, RefundPaymentOperation)
|
|
47
|
+
if err != nil {
|
|
48
|
+
log.Fatalln("Unable to register operations", err)
|
|
49
|
+
}
|
|
50
|
+
w.RegisterNexusService(service)
|
|
51
|
+
w.RegisterWorkflow(BillingChargeWorkflow) // handler workflows must also be registered
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Notes
|
|
55
|
+
|
|
56
|
+
- Imports: `"github.com/nexus-rpc/sdk-go/nexus"` for `nexus.NewService`, `nexus.NewSyncOperation`; `"go.temporal.io/sdk/temporalnexus"` for `NewWorkflowRunOperation`, `GetClient`
|
|
57
|
+
- Async operations (backed by workflows) use `temporalnexus.NewWorkflowRunOperation` — the workflow is started and the nexus operation resolves when the workflow completes
|
|
58
|
+
- Sync operations use `nexus.NewSyncOperation` — for direct computation or queries/signals via `temporalnexus.GetClient(ctx)`
|
|
59
|
+
- The service is registered on the handler worker (the target namespace's worker), not the caller
|
|
60
|
+
- Handler workflows must also be registered with `RegisterWorkflow` on the same worker
|
|
61
|
+
|
|
62
|
+
## When to use: sync vs async operations
|
|
63
|
+
|
|
64
|
+
- **Sync** (`nexus.NewSyncOperation`): must complete within 10 seconds. Use for short computations, querying a workflow, signaling a workflow, sending an update, calling external services/databases directly. If the handler times out, the caller's Nexus machinery auto-retries until ScheduleToCloseTimeout
|
|
65
|
+
- **Async / workflow-backed** (`temporalnexus.NewWorkflowRunOperation`): arbitrary duration. Use when the operation is a Temporal workflow. The caller receives a completion callback when the handler workflow finishes. Supports cancellation propagation and re-attachment via operation token
|
|
66
|
+
- The choice is static — it depends on which SDK builder function you use, not on runtime conditions
|
|
67
|
+
|
|
68
|
+
## Naming contract
|
|
69
|
+
|
|
70
|
+
- Operation names must match exactly at the wire level between caller and handler — no automatic transformation
|
|
71
|
+
- The string passed to `NewWorkflowRunOperation` or `NewSyncOperation` is the canonical operation name
|
|
72
|
+
- Define operation names as Go constants (as shown in the service contract section) and reference them from both caller and handler code
|
|
73
|
+
- In polyglot environments, both sides must agree on service name, operation names, and input/output types. Use Protobuf or JSON as the Data Converter format
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# nexus
|
|
2
|
+
|
|
3
|
+
## DSL
|
|
4
|
+
|
|
5
|
+
```twf
|
|
6
|
+
nexus BillingEndpoint BillingService.ChargePayment(order.payment) -> paymentResult
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Go
|
|
10
|
+
|
|
11
|
+
```go
|
|
12
|
+
c := workflow.NewNexusClient("BillingEndpoint", "BillingService")
|
|
13
|
+
var paymentResult PaymentResult
|
|
14
|
+
fut := c.ExecuteOperation(ctx, "ChargePayment", order.Payment, workflow.NexusOperationOptions{})
|
|
15
|
+
if err := fut.Get(ctx, &paymentResult); err != nil {
|
|
16
|
+
return Result{}, err
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Notes
|
|
21
|
+
|
|
22
|
+
- `workflow.NewNexusClient(endpoint, service)` creates a typed client scoped to one endpoint + service pair
|
|
23
|
+
- `ExecuteOperation(ctx, operationName, input, options)` starts the operation and returns a `NexusOperationFuture`
|
|
24
|
+
- The calling pattern mirrors child workflows — execute and `.Get()`
|
|
25
|
+
- For nexus options (timeouts), see [options.md](./options.md)
|
|
26
|
+
- For fire-and-forget nexus: see [detach.md](./detach.md)
|
|
27
|
+
- For nexus service definitions and handler registration: see [nexus-service-def.md](./nexus-service-def.md)
|
|
28
|
+
- Prefer referencing operation name constants from the service contract (see [nexus-service-def.md](./nexus-service-def.md#naming-contract)) rather than bare strings — keeps caller and handler in sync
|
|
29
|
+
|
|
30
|
+
## When to use Nexus vs child workflow
|
|
31
|
+
|
|
32
|
+
- **Use Nexus when:** crossing namespace boundaries (child workflows are same-namespace only in Temporal Cloud), isolating service boundaries (caller only knows endpoint name + operation — not namespace, task queue, retry policy, or workflow ID constraints), enabling polyglot interop (caller and handler can use different SDKs/languages), or enforcing blast-radius isolation between teams
|
|
33
|
+
- **Use child workflows when:** within a single namespace and team, partitioning event history, representing a resource with a unique workflow ID, or executing periodic logic via Continue-As-New
|
|
34
|
+
- Nexus adds Endpoint configuration overhead — if you don't need cross-namespace or service isolation, child workflows are simpler
|
|
35
|
+
- Nexus works within a single namespace too — you can start with Nexus and later split into separate namespaces with configuration changes only
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# options
|
|
2
|
+
|
|
3
|
+
## Activity options
|
|
4
|
+
|
|
5
|
+
### DSL
|
|
6
|
+
|
|
7
|
+
```twf
|
|
8
|
+
activity QuickLookup(data.id) -> result
|
|
9
|
+
options:
|
|
10
|
+
start_to_close_timeout: 30s
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Go
|
|
14
|
+
|
|
15
|
+
```go
|
|
16
|
+
actCtx := workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
|
|
17
|
+
StartToCloseTimeout: 30 * time.Second,
|
|
18
|
+
})
|
|
19
|
+
var result LookupResult
|
|
20
|
+
err := workflow.ExecuteActivity(actCtx, QuickLookup, data.Id).Get(ctx, &result)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Activity options with retry policy
|
|
24
|
+
|
|
25
|
+
### DSL
|
|
26
|
+
|
|
27
|
+
```twf
|
|
28
|
+
activity UnreliableService(data) -> result
|
|
29
|
+
options:
|
|
30
|
+
start_to_close_timeout: 2m
|
|
31
|
+
retry_policy:
|
|
32
|
+
maximum_attempts: 5
|
|
33
|
+
initial_interval: 1s
|
|
34
|
+
backoff_coefficient: 2.0
|
|
35
|
+
maximum_interval: 60s
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Go
|
|
39
|
+
|
|
40
|
+
```go
|
|
41
|
+
actCtx := workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
|
|
42
|
+
StartToCloseTimeout: 2 * time.Minute,
|
|
43
|
+
RetryPolicy: &temporal.RetryPolicy{
|
|
44
|
+
MaximumAttempts: 5,
|
|
45
|
+
InitialInterval: 1 * time.Second,
|
|
46
|
+
BackoffCoefficient: 2.0,
|
|
47
|
+
MaximumInterval: 60 * time.Second,
|
|
48
|
+
},
|
|
49
|
+
})
|
|
50
|
+
var result ServiceResult
|
|
51
|
+
err := workflow.ExecuteActivity(actCtx, UnreliableService, data).Get(ctx, &result)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Child workflow options
|
|
55
|
+
|
|
56
|
+
### DSL
|
|
57
|
+
|
|
58
|
+
```twf
|
|
59
|
+
workflow ChildWorkflow(input.data) -> childResult
|
|
60
|
+
options:
|
|
61
|
+
workflow_execution_timeout: 1h
|
|
62
|
+
retry_policy:
|
|
63
|
+
maximum_attempts: 3
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Go
|
|
67
|
+
|
|
68
|
+
```go
|
|
69
|
+
childCtx := workflow.WithChildOptions(ctx, workflow.ChildWorkflowOptions{
|
|
70
|
+
WorkflowExecutionTimeout: 1 * time.Hour,
|
|
71
|
+
RetryPolicy: &temporal.RetryPolicy{
|
|
72
|
+
MaximumAttempts: 3,
|
|
73
|
+
},
|
|
74
|
+
})
|
|
75
|
+
var childResult ChildResult
|
|
76
|
+
err := workflow.ExecuteChildWorkflow(childCtx, ChildWorkflow, input.Data).Get(ctx, &childResult)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Child workflow `parent_close_policy`
|
|
80
|
+
|
|
81
|
+
### DSL
|
|
82
|
+
|
|
83
|
+
```twf
|
|
84
|
+
workflow NotifyCustomer(order.customer)
|
|
85
|
+
options:
|
|
86
|
+
parent_close_policy: REQUEST_CANCEL
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Go
|
|
90
|
+
|
|
91
|
+
```go
|
|
92
|
+
childCtx := workflow.WithChildOptions(ctx, workflow.ChildWorkflowOptions{
|
|
93
|
+
ParentClosePolicy: enumspb.PARENT_CLOSE_POLICY_REQUEST_CANCEL,
|
|
94
|
+
})
|
|
95
|
+
err := workflow.ExecuteChildWorkflow(childCtx, NotifyCustomer, order.Customer).Get(ctx, nil)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Nexus operation options
|
|
99
|
+
|
|
100
|
+
### DSL
|
|
101
|
+
|
|
102
|
+
```twf
|
|
103
|
+
nexus BillingEndpoint BillingService.ChargePayment(payment) -> paymentResult
|
|
104
|
+
options:
|
|
105
|
+
schedule_to_close_timeout: 1h
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Go
|
|
109
|
+
|
|
110
|
+
```go
|
|
111
|
+
c := workflow.NewNexusClient("BillingEndpoint", "BillingService")
|
|
112
|
+
var paymentResult PaymentResult
|
|
113
|
+
fut := c.ExecuteOperation(ctx, "ChargePayment", payment, workflow.NexusOperationOptions{
|
|
114
|
+
ScheduleToCloseTimeout: 1 * time.Hour,
|
|
115
|
+
})
|
|
116
|
+
err := fut.Get(ctx, &paymentResult)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Notes
|
|
120
|
+
|
|
121
|
+
- When no `options:` block is specified, set a default `ActivityOptions` with `StartToCloseTimeout` on `ctx` near the top of the workflow function
|
|
122
|
+
- Option keys map: `start_to_close_timeout` → `StartToCloseTimeout`, `schedule_to_close_timeout` → `ScheduleToCloseTimeout`, `heartbeat_timeout` → `HeartbeatTimeout`, `parent_close_policy` → `ParentClosePolicy` (type: `enumspb.ParentClosePolicy`; values: `PARENT_CLOSE_POLICY_TERMINATE` (default), `PARENT_CLOSE_POLICY_REQUEST_CANCEL`, `PARENT_CLOSE_POLICY_ABANDON`)
|
|
123
|
+
- `retry_policy:` → `&temporal.RetryPolicy{...}` (pointer)
|
|
124
|
+
- `NexusOperationOptions` fields: `ScheduleToCloseTimeout` (primary) and `CancellationType` (experimental). Options are passed inline — no context wrapping like activities
|
|
125
|
+
|
|
126
|
+
## When to use each timeout
|
|
127
|
+
|
|
128
|
+
- **`StartToCloseTimeout`** — maximum time for a single activity attempt. Resets on each retry. Primary mechanism for detecting worker crashes. Temporal recommends always setting this
|
|
129
|
+
- **`ScheduleToCloseTimeout`** — total wall-clock time from when the activity is scheduled, including all retries. Does not reset. Use to cap total time when retries have exponential backoff
|
|
130
|
+
- **`WorkflowExecutionTimeout`** — end-to-end timeout for the entire workflow execution including retries and Continue-As-New chains. Set on the client when starting the workflow, not in `ActivityOptions`
|
|
131
|
+
- At least one of `StartToCloseTimeout` or `ScheduleToCloseTimeout` is required for activities — omitting both is a runtime error
|
|
132
|
+
|
|
133
|
+
## Pitfalls
|
|
134
|
+
|
|
135
|
+
- **`MaximumAttempts: 1`** means one attempt total — this disables retries. `MaximumAttempts: 0` (the default) means unlimited retries
|
|
136
|
+
- **`BackoffCoefficient: 1.0`** produces fixed-interval retries (no exponential growth). The formula is `InitialInterval * BackoffCoefficient^(attempt-1)`
|
|
137
|
+
- Activities retry by default (server default: initial interval 1s, backoff 2.0, max interval 100s, unlimited attempts). Workflows do not retry by default
|
|
138
|
+
- If no `RetryPolicy` is specified on an activity, the server default applies — this is a common source of surprise
|