@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,230 @@
|
|
|
1
|
+
# Long-Running Workflows
|
|
2
|
+
|
|
3
|
+
> **Example:** [`long-running.twf`](./long-running.twf)
|
|
4
|
+
|
|
5
|
+
Patterns for workflows that run for extended periods: continue-as-new, history management, and entity workflows.
|
|
6
|
+
|
|
7
|
+
## The History Problem
|
|
8
|
+
|
|
9
|
+
Temporal replays the full event history to reconstruct workflow state after any restart. This is the **primary constraint** on long-running workflows — history size directly determines replay cost, and Temporal enforces a hard limit (~50MB / ~50K events).
|
|
10
|
+
|
|
11
|
+
| Issue | Impact |
|
|
12
|
+
|-------|--------|
|
|
13
|
+
| Replay cost | **Entire history replayed on every recovery** — this is the main bottleneck |
|
|
14
|
+
| Hard limit | ~50MB event history / ~50K events — workflow terminates if exceeded |
|
|
15
|
+
| Memory | Full history loaded into worker memory during replay |
|
|
16
|
+
| Latency | Longer history = slower recovery after worker restart |
|
|
17
|
+
|
|
18
|
+
**Solution:** Reset history periodically with `continue_as_new`.
|
|
19
|
+
|
|
20
|
+
> **A bound is not a free pass.** "Only infinite loops need `continue_as_new`" is wrong. A *bounded* loop still grows history linearly with its bound, and if per-iteration history is chunky (a large activity result plus several tool calls each iteration) it can hit the limit well before the bound. The rule is: **loops whose accumulated history is large need `continue_as_new` — the bound alone is not sufficient.**
|
|
21
|
+
>
|
|
22
|
+
> **State the strategy explicitly.** Even though *where* to continue-as-new is partly an implementation concern, the design should **say** what it is — "bounded at N, per-iteration history small, no `continue_as_new`," "resets every K iterations," or "defer to author-go" — rather than leaving it silent. A silent design is one nobody decided.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Continue-As-New
|
|
27
|
+
|
|
28
|
+
Atomically complete current workflow and start a new execution with fresh history, preserving logical continuity.
|
|
29
|
+
|
|
30
|
+
### Basic Pattern
|
|
31
|
+
|
|
32
|
+
```twf
|
|
33
|
+
workflow LongRunningProcessor(processor: Processor):
|
|
34
|
+
eventCount = 0
|
|
35
|
+
|
|
36
|
+
for:
|
|
37
|
+
await signal NewEvent -> (event)
|
|
38
|
+
activity ProcessEvent(event)
|
|
39
|
+
processor.processed += 1
|
|
40
|
+
eventCount += 1
|
|
41
|
+
|
|
42
|
+
# Reset history before it gets too large
|
|
43
|
+
if eventCount >= 1000:
|
|
44
|
+
close continue_as_new(processor) # Fresh history, same logical workflow
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Continue-As-New Semantics
|
|
48
|
+
|
|
49
|
+
| Aspect | Behavior |
|
|
50
|
+
|--------|----------|
|
|
51
|
+
| Workflow ID | Same (logical continuity) |
|
|
52
|
+
| Run ID | New (fresh execution) |
|
|
53
|
+
| History | Reset to zero |
|
|
54
|
+
| Pending signals | Carried over (configurable) |
|
|
55
|
+
| State | Passed as input to new execution |
|
|
56
|
+
|
|
57
|
+
### When to Continue-As-New
|
|
58
|
+
|
|
59
|
+
| Trigger | Example |
|
|
60
|
+
|---------|---------|
|
|
61
|
+
| Event count | After processing N events |
|
|
62
|
+
| Time-based | Every 24 hours |
|
|
63
|
+
| History size | Approaching limit |
|
|
64
|
+
| Periodic reset | End of billing cycle |
|
|
65
|
+
|
|
66
|
+
### SDK Intrinsics for History Tracking
|
|
67
|
+
|
|
68
|
+
These deterministic SDK functions are available in workflow code (not activities) for deciding when to continue-as-new:
|
|
69
|
+
|
|
70
|
+
| Function | Returns | Use |
|
|
71
|
+
|----------|---------|-----|
|
|
72
|
+
| `workflow.history_length()` | Event count | Compare against threshold (e.g., `>= 1000`) |
|
|
73
|
+
| `workflow.history_size()` | Bytes | Compare against limit (e.g., `> 40_000_000`) |
|
|
74
|
+
|
|
75
|
+
These appear in TWF as raw expressions since they're SDK-level calls, not TWF keywords.
|
|
76
|
+
|
|
77
|
+
### Data Serialization
|
|
78
|
+
|
|
79
|
+
```twf
|
|
80
|
+
workflow EntityWorkflow(entity: Entity, data: EntityData):
|
|
81
|
+
for:
|
|
82
|
+
await signal Command -> (command)
|
|
83
|
+
data = applyCommand(data, command)
|
|
84
|
+
|
|
85
|
+
# Periodic continuation with current data
|
|
86
|
+
if should_continue():
|
|
87
|
+
close continue_as_new(entity, data)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
> Note: Data structs are defined at the SDK level, not in TWF notation.
|
|
91
|
+
|
|
92
|
+
```pseudo
|
|
93
|
+
# Data must be serializable!
|
|
94
|
+
struct EntityData:
|
|
95
|
+
balance: decimal
|
|
96
|
+
lastUpdated: timestamp
|
|
97
|
+
pendingOperations: []Operation
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Entity Workflow Pattern
|
|
103
|
+
|
|
104
|
+
Long-lived workflow representing a business entity (user, order, account, subscription).
|
|
105
|
+
|
|
106
|
+
### Structure
|
|
107
|
+
|
|
108
|
+
```twf
|
|
109
|
+
workflow UserEntity(userId: string, user: User):
|
|
110
|
+
# Initialize user if new
|
|
111
|
+
if user == null:
|
|
112
|
+
activity LoadUser(userId) -> (user)
|
|
113
|
+
|
|
114
|
+
query GetUser() -> (User):
|
|
115
|
+
return user
|
|
116
|
+
|
|
117
|
+
update UpdateSettings(settings: Settings) -> (Result):
|
|
118
|
+
user.settings = settings
|
|
119
|
+
return Result{success: true}
|
|
120
|
+
|
|
121
|
+
for:
|
|
122
|
+
# Wait for commands or periodic triggers
|
|
123
|
+
await one:
|
|
124
|
+
signal UpdateProfile:
|
|
125
|
+
user.profile = signal.data
|
|
126
|
+
|
|
127
|
+
signal AddCredits:
|
|
128
|
+
user.credits += signal.amount
|
|
129
|
+
|
|
130
|
+
signal Deactivate:
|
|
131
|
+
user.active = false
|
|
132
|
+
close complete # End entity lifecycle
|
|
133
|
+
|
|
134
|
+
timer(24h):
|
|
135
|
+
# Periodic maintenance
|
|
136
|
+
|
|
137
|
+
# Persist after any change
|
|
138
|
+
activity PersistUser(user)
|
|
139
|
+
|
|
140
|
+
# Continue-as-new periodically
|
|
141
|
+
if eventCount > 500:
|
|
142
|
+
close continue_as_new(userId, user)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Entity Lifecycle
|
|
146
|
+
|
|
147
|
+
> Note: Entity lifecycle management uses SDK-level API calls, not TWF notation.
|
|
148
|
+
|
|
149
|
+
```pseudo
|
|
150
|
+
# Create entity (start workflow)
|
|
151
|
+
temporal.start_workflow(
|
|
152
|
+
workflow: UserEntity,
|
|
153
|
+
id: "user-{userId}",
|
|
154
|
+
input: {userId: userId, user: null}
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Interact with entity (signals, queries, updates)
|
|
158
|
+
temporal.signal("user-{userId}", UpdateProfile, {name: "Alice"})
|
|
159
|
+
user = temporal.query("user-{userId}", GetUser)
|
|
160
|
+
result = temporal.update("user-{userId}", AddCredits, {amount: 100})
|
|
161
|
+
|
|
162
|
+
# Entity continues until explicit termination
|
|
163
|
+
temporal.signal("user-{userId}", Deactivate, {})
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Entity vs Process Workflows
|
|
167
|
+
|
|
168
|
+
| Entity Workflow | Process Workflow |
|
|
169
|
+
|-----------------|------------------|
|
|
170
|
+
| Long-lived (days, months, years) | Short-lived (minutes, hours) |
|
|
171
|
+
| Represents a thing | Represents a process |
|
|
172
|
+
| Reacts to external events | Drives toward completion |
|
|
173
|
+
| No natural end state | Has completion state |
|
|
174
|
+
| Examples: User, Account, Subscription | Examples: Order, Deployment, Migration |
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Continue-As-New Anti-Patterns
|
|
179
|
+
|
|
180
|
+
### Losing Data on Continue
|
|
181
|
+
|
|
182
|
+
```twf
|
|
183
|
+
# BAD: Data not passed to continuation
|
|
184
|
+
workflow Processor(data: ProcessorData):
|
|
185
|
+
modifiedData = transform(data)
|
|
186
|
+
close continue_as_new() # Lost modifiedData!
|
|
187
|
+
|
|
188
|
+
# GOOD: Pass current data
|
|
189
|
+
workflow Processor(data: ProcessorData):
|
|
190
|
+
modifiedData = transform(data)
|
|
191
|
+
close continue_as_new(modifiedData)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Continue-As-New in Wrong Place
|
|
195
|
+
|
|
196
|
+
```twf
|
|
197
|
+
# BAD: Continue in middle of operation
|
|
198
|
+
workflow Processor(data: ProcessorData):
|
|
199
|
+
activity Step1()
|
|
200
|
+
if shouldContinue:
|
|
201
|
+
close continue_as_new(data) # Step2 never runs!
|
|
202
|
+
activity Step2()
|
|
203
|
+
|
|
204
|
+
# GOOD: Continue at natural boundary
|
|
205
|
+
workflow Processor(data: ProcessorData):
|
|
206
|
+
activity Step1()
|
|
207
|
+
activity Step2()
|
|
208
|
+
if shouldContinue:
|
|
209
|
+
close continue_as_new(data)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Too Frequent Continuation
|
|
213
|
+
|
|
214
|
+
```twf
|
|
215
|
+
# BAD: Continue every event
|
|
216
|
+
workflow Processor(data: ProcessorData):
|
|
217
|
+
event = await signal Event
|
|
218
|
+
process(event)
|
|
219
|
+
close continue_as_new(data) # Unnecessary overhead!
|
|
220
|
+
|
|
221
|
+
# GOOD: Batch before continuing
|
|
222
|
+
workflow Processor(data: ProcessorData):
|
|
223
|
+
count = 0
|
|
224
|
+
for:
|
|
225
|
+
event = await signal Event
|
|
226
|
+
process(event)
|
|
227
|
+
count += 1
|
|
228
|
+
if count >= 1000:
|
|
229
|
+
close continue_as_new(data)
|
|
230
|
+
```
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Source: long-running.md
|
|
2
|
+
# Patterns: continue_as_new, entity workflow, signal/query/update
|
|
3
|
+
|
|
4
|
+
# --- Basic continue-as-new pattern ---
|
|
5
|
+
|
|
6
|
+
workflow LongRunningProcessor(processor: Processor):
|
|
7
|
+
signal NewEvent(data: EventData):
|
|
8
|
+
processor.lastEvent = data
|
|
9
|
+
|
|
10
|
+
eventCount = 0
|
|
11
|
+
|
|
12
|
+
for:
|
|
13
|
+
await one:
|
|
14
|
+
signal NewEvent:
|
|
15
|
+
activity ProcessEvent(processor) -> result
|
|
16
|
+
processor.processed = processor.processed + 1
|
|
17
|
+
eventCount = eventCount + 1
|
|
18
|
+
timer(24h):
|
|
19
|
+
activity PeriodicCheck(processor)
|
|
20
|
+
|
|
21
|
+
# Reset history before it gets too large
|
|
22
|
+
if (eventCount >= 1000):
|
|
23
|
+
close continue_as_new(processor)
|
|
24
|
+
|
|
25
|
+
# --- Entity workflow pattern ---
|
|
26
|
+
|
|
27
|
+
workflow UserEntity(userId: string, user: User):
|
|
28
|
+
signal UpdateProfile(data: ProfileData):
|
|
29
|
+
user.profile = data
|
|
30
|
+
|
|
31
|
+
signal AddCredits(amount: decimal):
|
|
32
|
+
user.credits = user.credits + amount
|
|
33
|
+
|
|
34
|
+
signal Deactivate():
|
|
35
|
+
user.active = false
|
|
36
|
+
|
|
37
|
+
query GetUser() -> (User):
|
|
38
|
+
return user
|
|
39
|
+
|
|
40
|
+
update UpdateSettings(settings: Settings) -> (Result):
|
|
41
|
+
user.settings = settings
|
|
42
|
+
activity PersistUser(user)
|
|
43
|
+
return Result{success: true}
|
|
44
|
+
|
|
45
|
+
# Initialize user if new
|
|
46
|
+
if (user == null):
|
|
47
|
+
activity LoadUser(userId) -> user
|
|
48
|
+
|
|
49
|
+
eventCount = 0
|
|
50
|
+
|
|
51
|
+
for:
|
|
52
|
+
await one:
|
|
53
|
+
signal UpdateProfile:
|
|
54
|
+
activity PersistUser(user)
|
|
55
|
+
signal AddCredits:
|
|
56
|
+
activity PersistUser(user)
|
|
57
|
+
signal Deactivate:
|
|
58
|
+
activity PersistUser(user)
|
|
59
|
+
close complete
|
|
60
|
+
timer(24h):
|
|
61
|
+
activity DailyMaintenance(user)
|
|
62
|
+
|
|
63
|
+
eventCount = eventCount + 1
|
|
64
|
+
|
|
65
|
+
# Continue-as-new periodically
|
|
66
|
+
if (eventCount > 500):
|
|
67
|
+
close continue_as_new(userId, user)
|
|
68
|
+
|
|
69
|
+
# --- Supporting activities ---
|
|
70
|
+
|
|
71
|
+
activity ProcessEvent(processor: Processor) -> (EventResult):
|
|
72
|
+
return process(processor)
|
|
73
|
+
|
|
74
|
+
activity LoadUser(userId: string) -> (User):
|
|
75
|
+
return db.load(userId)
|
|
76
|
+
|
|
77
|
+
activity PersistUser(user: User):
|
|
78
|
+
db.save(user.id, user)
|
|
79
|
+
|
|
80
|
+
activity DailyMaintenance(user: User):
|
|
81
|
+
run_maintenance(user.id, user)
|
|
82
|
+
|
|
83
|
+
activity PeriodicCheck(processor: Processor):
|
|
84
|
+
check(processor)
|
|
85
|
+
|
|
86
|
+
# --- Worker and namespace ---
|
|
87
|
+
|
|
88
|
+
worker longRunningWorker:
|
|
89
|
+
workflow LongRunningProcessor
|
|
90
|
+
workflow UserEntity
|
|
91
|
+
activity ProcessEvent
|
|
92
|
+
activity LoadUser
|
|
93
|
+
activity PersistUser
|
|
94
|
+
activity DailyMaintenance
|
|
95
|
+
activity PeriodicCheck
|
|
96
|
+
|
|
97
|
+
namespace longRunning:
|
|
98
|
+
worker longRunningWorker
|
|
99
|
+
options:
|
|
100
|
+
task_queue: "long-running"
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# Nexus: Cross-Namespace Communication
|
|
2
|
+
|
|
3
|
+
> **Example:** [`nexus.twf`](./nexus.twf)
|
|
4
|
+
|
|
5
|
+
Nexus enables workflows in one Temporal namespace to call operations in another namespace, with proper authorization and abstraction.
|
|
6
|
+
|
|
7
|
+
## When to Use Nexus
|
|
8
|
+
|
|
9
|
+
| Use Nexus | Use Child Workflow Instead |
|
|
10
|
+
|-----------|---------------------------|
|
|
11
|
+
| Cross-namespace calls | Same namespace |
|
|
12
|
+
| Cross-team boundaries | Same team |
|
|
13
|
+
| Different security contexts | Same security context |
|
|
14
|
+
| Service abstraction needed | Direct coupling acceptable |
|
|
15
|
+
| Multi-tenant architectures | Single-tenant |
|
|
16
|
+
|
|
17
|
+
> **Deciding how many namespaces?** See [namespaces.md](../reference/namespaces.md) — the default is **one**. Nexus is the mechanism for the *one* case that legitimately spans namespaces (a cross-team / different-security-context service contract); it is not a reason to multiply namespaces. Same-namespace calls should be child workflows, not Nexus.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Nexus Concepts
|
|
22
|
+
|
|
23
|
+
### Architecture
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
orders Namespace (Caller)
|
|
27
|
+
OrderCheckout Workflow
|
|
28
|
+
nexus PaymentsEndpoint PaymentsService.ProcessPayment(args) -> result
|
|
29
|
+
|
|
|
30
|
+
v (cross-namespace)
|
|
31
|
+
payments Namespace (Target)
|
|
32
|
+
PaymentsEndpoint (task_queue: "payments")
|
|
33
|
+
PaymentsService
|
|
34
|
+
async ProcessPayment -> starts ProcessPaymentWorkflow
|
|
35
|
+
|
|
36
|
+
orders Namespace (Caller)
|
|
37
|
+
OrderCheckout Workflow
|
|
38
|
+
detach nexus NotificationsEndpoint NotificationsService.SendConfirmation(args)
|
|
39
|
+
|
|
|
40
|
+
v (cross-namespace)
|
|
41
|
+
notifications Namespace (Target)
|
|
42
|
+
NotificationsEndpoint (task_queue: "notifications")
|
|
43
|
+
NotificationsService
|
|
44
|
+
async SendConfirmation -> starts SendConfirmationWorkflow
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Components
|
|
48
|
+
|
|
49
|
+
| Component | TWF Construct | Description |
|
|
50
|
+
|-----------|--------------|-------------|
|
|
51
|
+
| **Nexus Service** | `nexus service Name:` | Top-level definition with operations |
|
|
52
|
+
| **Async Operation** | `async OpName workflow WorkflowName` | Delegates to a named workflow |
|
|
53
|
+
| **Sync Operation** | `sync OpName(params) -> (Type):` | Runs inline with a body |
|
|
54
|
+
| **Nexus Endpoint** | `nexus endpoint Name` (in namespace) | Deployment routing with `task_queue` |
|
|
55
|
+
| **Service Reference** | `nexus service Name` (in worker) | Links service to worker |
|
|
56
|
+
| **Nexus Call** | `nexus Endpoint Service.Op(args)` | Invokes an operation |
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Nexus Service Definition
|
|
61
|
+
|
|
62
|
+
Define a nexus service with typed operations:
|
|
63
|
+
|
|
64
|
+
```twf
|
|
65
|
+
nexus service PaymentsService:
|
|
66
|
+
async ProcessPayment workflow ProcessPaymentWorkflow
|
|
67
|
+
sync GetPaymentStatus(paymentId: string) -> (PaymentStatus):
|
|
68
|
+
activity LookupPayment(paymentId) -> status
|
|
69
|
+
close complete(status)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
- **Async operations** delegate to a named workflow (one-liner, no body)
|
|
73
|
+
- **Sync operations** have a body using the workflow statement set
|
|
74
|
+
|
|
75
|
+
### Deployment
|
|
76
|
+
|
|
77
|
+
Each Nexus service lives in its own namespace. The endpoint is defined alongside the worker that serves it, in the target namespace. The caller namespace only hosts the workflows that invoke the endpoints.
|
|
78
|
+
|
|
79
|
+
```twf
|
|
80
|
+
worker paymentProcessingWorker:
|
|
81
|
+
workflow ProcessPaymentWorkflow
|
|
82
|
+
activity LookupPayment
|
|
83
|
+
nexus service PaymentsService
|
|
84
|
+
|
|
85
|
+
# Target namespace: owns the service and exposes the endpoint
|
|
86
|
+
namespace payments:
|
|
87
|
+
worker paymentProcessingWorker
|
|
88
|
+
options:
|
|
89
|
+
task_queue: "payments"
|
|
90
|
+
nexus endpoint PaymentsEndpoint
|
|
91
|
+
options:
|
|
92
|
+
task_queue: "payments"
|
|
93
|
+
|
|
94
|
+
# Caller namespace: only has the workflows that call into payments
|
|
95
|
+
namespace orders:
|
|
96
|
+
worker checkoutWorker
|
|
97
|
+
options:
|
|
98
|
+
task_queue: "checkout"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Nexus Call Syntax
|
|
104
|
+
|
|
105
|
+
### Basic Call
|
|
106
|
+
|
|
107
|
+
```twf
|
|
108
|
+
nexus PaymentsEndpoint PaymentsService.ProcessPayment(order.payment) -> result
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Three identifiers: `Endpoint Service.Operation(args)` — endpoint name, then service and operation separated by a dot.
|
|
112
|
+
|
|
113
|
+
### With Options
|
|
114
|
+
|
|
115
|
+
```twf
|
|
116
|
+
nexus PaymentsEndpoint PaymentsService.ProcessPayment(payment) -> result
|
|
117
|
+
options:
|
|
118
|
+
schedule_to_close_timeout: 5m
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Options: `schedule_to_close_timeout`, `retry_policy`, `priority`.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Execution Modes
|
|
126
|
+
|
|
127
|
+
Nexus calls support the same three execution modes as child workflows:
|
|
128
|
+
|
|
129
|
+
| Mode | Syntax | Behavior |
|
|
130
|
+
|------|--------|----------|
|
|
131
|
+
| **Synchronous** | `nexus Ep Svc.Op(args) -> result` | Caller blocks until operation completes |
|
|
132
|
+
| **Async (promise)** | `promise p <- nexus Ep Svc.Op(args)` | Caller continues, awaits promise later |
|
|
133
|
+
| **Fire-and-forget** | `detach nexus Ep Svc.Op(args)` | Caller continues, never waits |
|
|
134
|
+
|
|
135
|
+
### Synchronous (Default)
|
|
136
|
+
|
|
137
|
+
```twf
|
|
138
|
+
workflow Caller(order: Order) -> (Result):
|
|
139
|
+
nexus PaymentsEndpoint PaymentsService.ProcessPayment(order.payment) -> result
|
|
140
|
+
close complete(Result{paymentId: result.id})
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Asynchronous (Promise)
|
|
144
|
+
|
|
145
|
+
```twf
|
|
146
|
+
workflow Caller(data: Data) -> (Result):
|
|
147
|
+
promise handle <- nexus PaymentsEndpoint PaymentsService.ProcessPayment(data.payment)
|
|
148
|
+
activity DoOtherWork(data) -> localResult
|
|
149
|
+
await handle -> paymentResult
|
|
150
|
+
close complete(Result{localResult, paymentResult})
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Fire-and-Forget (Detach)
|
|
154
|
+
|
|
155
|
+
```twf
|
|
156
|
+
workflow Caller(order: Order) -> (Result):
|
|
157
|
+
detach nexus NotificationsEndpoint NotificationsService.SendConfirmation(order.customer)
|
|
158
|
+
close complete(Result{status: "initiated"})
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Await Patterns
|
|
164
|
+
|
|
165
|
+
### Await Nexus
|
|
166
|
+
|
|
167
|
+
```twf
|
|
168
|
+
await nexus PaymentsEndpoint PaymentsService.GetStatus(id) -> status
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Await One with Nexus
|
|
172
|
+
|
|
173
|
+
Race a nexus call against a timeout:
|
|
174
|
+
|
|
175
|
+
```twf
|
|
176
|
+
workflow Caller(data: Data) -> (Result):
|
|
177
|
+
await one:
|
|
178
|
+
nexus PaymentsEndpoint PaymentsService.ProcessPayment(data) -> result:
|
|
179
|
+
close complete(Result{success: true, data: result})
|
|
180
|
+
timer(5m):
|
|
181
|
+
activity AlertTimeout(data)
|
|
182
|
+
close fail(Result{success: false, error: "timeout"})
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
> **The nexus operation continues if the timer wins.** Losing an `await one` race does **not** cancel the nexus call — the operation (and the workflow it runs in the *target* namespace) keeps running until this workflow run ends. Since the target is an independent service, you usually can't cancel it implicitly; if the payment must be voided on timeout, model that explicitly (a compensating nexus op or activity), not by relying on the race.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Resolution
|
|
190
|
+
|
|
191
|
+
The resolver validates all nexus references:
|
|
192
|
+
|
|
193
|
+
### Errors
|
|
194
|
+
|
|
195
|
+
| Condition | Error |
|
|
196
|
+
|-----------|-------|
|
|
197
|
+
| Duplicate `nexus service` name | `duplicate nexus service definition: X` |
|
|
198
|
+
| Duplicate endpoint name across namespaces | `duplicate nexus endpoint name "X"` |
|
|
199
|
+
| Endpoint not found (endpoints exist) | `undefined nexus endpoint: X` |
|
|
200
|
+
| Service not found (services exist) | `undefined nexus service: X` |
|
|
201
|
+
| Operation not found on service | `nexus service X has no operation Y` |
|
|
202
|
+
| `detach nexus ... -> result` | `detach nexus call cannot have a result` |
|
|
203
|
+
| Async op references missing workflow | `async operation Y references undefined workflow: Z` |
|
|
204
|
+
| Worker refs missing service | `worker W references undefined nexus service: X` |
|
|
205
|
+
| Endpoint missing `task_queue` | `nexus endpoint X missing required task_queue option` |
|
|
206
|
+
| Endpoint task queue has no worker with service | `no worker on that queue has service S` |
|
|
207
|
+
|
|
208
|
+
### Warnings
|
|
209
|
+
|
|
210
|
+
| Condition | Warning |
|
|
211
|
+
|-----------|---------|
|
|
212
|
+
| Service not on any worker (namespaces exist) | `nexus service X is not referenced by any worker` |
|
|
213
|
+
| Endpoint not found (no endpoints defined) | `unresolved nexus endpoint: X (may be external)` |
|
|
214
|
+
| Service not found (no services defined) | `unresolved nexus service: X (may be external)` |
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Anti-Patterns
|
|
219
|
+
|
|
220
|
+
### Nexus for Same-Namespace Calls
|
|
221
|
+
|
|
222
|
+
Nexus adds routing and authorization overhead that is only justified across namespace boundaries. Calling a service in the same namespace should use a child workflow instead.
|
|
223
|
+
|
|
224
|
+
```twf
|
|
225
|
+
# BAD: Nexus overhead for a call that stays inside the orders namespace
|
|
226
|
+
workflow OrderCheckout(order: Order) -> (OrderResult):
|
|
227
|
+
nexus LocalEndpoint LocalService.Validate(order) -> result
|
|
228
|
+
|
|
229
|
+
# GOOD: Child workflow — same namespace, same team, no boundary to cross
|
|
230
|
+
workflow OrderCheckout(order: Order) -> (OrderResult):
|
|
231
|
+
workflow ValidateOrder(order) -> result
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Missing Timeout
|
|
235
|
+
|
|
236
|
+
```twf
|
|
237
|
+
# BAD: No deadline
|
|
238
|
+
workflow A():
|
|
239
|
+
nexus Endpoint Svc.SlowOperation(data) -> result
|
|
240
|
+
|
|
241
|
+
# GOOD: Explicit deadline via await one
|
|
242
|
+
workflow A():
|
|
243
|
+
await one:
|
|
244
|
+
nexus Endpoint Svc.SlowOperation(data) -> result:
|
|
245
|
+
close complete(Result{result})
|
|
246
|
+
timer(5m):
|
|
247
|
+
close fail(Result{error: "timeout"})
|
|
248
|
+
```
|