@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,332 @@
|
|
|
1
|
+
# Common Anti-Patterns
|
|
2
|
+
|
|
3
|
+
> **Re-check pass (required).** This catalog is not just reference — during the [Design Review](../SKILL.md#design-review), walk the *finished* design against **every** anti-pattern below and confirm none apply. Designs drift into these shapes by copying prior work uncritically; pattern-matching against a catalog of bad shapes is exactly what this pass is for. The checklist line is: "Design re-checked against every anti-pattern in `anti-patterns.md`."
|
|
4
|
+
|
|
5
|
+
## Structural
|
|
6
|
+
|
|
7
|
+
### Unbounded History
|
|
8
|
+
|
|
9
|
+
A workflow that runs indefinitely without resetting accumulates unbounded event history, eventually degrading performance.
|
|
10
|
+
|
|
11
|
+
```twf
|
|
12
|
+
# BAD: Infinite loop with no history reset
|
|
13
|
+
# workflow EventProcessor(config: Config):
|
|
14
|
+
# for:
|
|
15
|
+
# activity PollEvents(config) -> events
|
|
16
|
+
# activity ProcessBatch(events)
|
|
17
|
+
|
|
18
|
+
# GOOD: Continue-as-new resets history periodically
|
|
19
|
+
workflow EventProcessor(config: Config):
|
|
20
|
+
state:
|
|
21
|
+
condition shutdownRequested
|
|
22
|
+
signal Shutdown():
|
|
23
|
+
set shutdownRequested
|
|
24
|
+
for:
|
|
25
|
+
if (shutdownRequested):
|
|
26
|
+
close complete
|
|
27
|
+
activity PollEvents(config) -> events
|
|
28
|
+
activity ProcessBatch(events)
|
|
29
|
+
close continue_as_new(config)
|
|
30
|
+
|
|
31
|
+
activity PollEvents(config: Config) -> (Events):
|
|
32
|
+
return poll(config)
|
|
33
|
+
|
|
34
|
+
activity ProcessBatch(events: Events):
|
|
35
|
+
process(events)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Why:** Temporal stores every event in workflow history. Long-running workflows without `close continue_as_new` grow history without bound, causing slow replays and eventual failure. See [long-running.md](../topics/long-running.md).
|
|
39
|
+
|
|
40
|
+
> **Bounded is not automatically safe.** The rule is not "infinite loops need `continue_as_new`" — it's "**loops whose accumulated history is large need `continue_as_new`; a bound alone is not sufficient.**" A loop with an internal bound (e.g. `for` over 40 iterations) still grows history linearly, and if per-iteration history is chunky (a large `LlmCall` result plus N tool calls each iteration) or the bound is high, it can blow the limit before finishing. State the strategy explicitly in the design — "bounded at N, per-iteration history small, no `continue_as_new`" or "resets every K iterations" — rather than leaving it silent.
|
|
41
|
+
|
|
42
|
+
### Wrapper Workflow
|
|
43
|
+
|
|
44
|
+
A child workflow containing a single activity call adds orchestration overhead with no benefit.
|
|
45
|
+
|
|
46
|
+
```pseudo
|
|
47
|
+
# BAD: Unnecessary child workflow wrapper
|
|
48
|
+
workflow Parent():
|
|
49
|
+
workflow SendEmailWorkflow(to, body)
|
|
50
|
+
|
|
51
|
+
workflow SendEmailWorkflow(to, body):
|
|
52
|
+
activity SendEmail(to, body)
|
|
53
|
+
close complete
|
|
54
|
+
|
|
55
|
+
# GOOD: Call the activity directly
|
|
56
|
+
workflow Parent():
|
|
57
|
+
activity SendEmail(to, body)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Why:** Child workflows create separate history, require their own task queue routing, and add latency. Use them only when you need independent retry policies, a separate failure boundary, or multi-step orchestration.
|
|
61
|
+
|
|
62
|
+
### Monolithic Workflow
|
|
63
|
+
|
|
64
|
+
All business logic in a single workflow with dozens of sequential steps.
|
|
65
|
+
|
|
66
|
+
```pseudo
|
|
67
|
+
# BAD: One workflow doing everything
|
|
68
|
+
workflow ProcessOrder(order):
|
|
69
|
+
activity Validate(order)
|
|
70
|
+
activity CheckInventory(order)
|
|
71
|
+
activity ReserveInventory(order)
|
|
72
|
+
activity ChargePayment(order)
|
|
73
|
+
activity CreateShipment(order)
|
|
74
|
+
activity NotifyWarehouse(order)
|
|
75
|
+
activity UpdateCRM(order)
|
|
76
|
+
activity SendConfirmation(order)
|
|
77
|
+
activity ScheduleFollowUp(order)
|
|
78
|
+
# ... 20 more steps
|
|
79
|
+
|
|
80
|
+
# GOOD: Decompose into child workflows with clear boundaries
|
|
81
|
+
workflow ProcessOrder(order):
|
|
82
|
+
activity ValidateOrder(order) -> validated
|
|
83
|
+
workflow FulfillOrder(validated) -> fulfillment
|
|
84
|
+
workflow NotifyStakeholders(order, fulfillment)
|
|
85
|
+
close complete(OrderResult{fulfillment})
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Why:** Large workflows have large histories (slow replay), make failure recovery coarse-grained (one failure may require re-running unrelated steps), and are hard to test. Decompose when a group of steps has its own lifecycle, retry needs, or failure boundary.
|
|
89
|
+
|
|
90
|
+
### Large Payloads in Workflow State
|
|
91
|
+
|
|
92
|
+
Storing large data (files, full database results, images) in workflow variables or signal/update payloads.
|
|
93
|
+
|
|
94
|
+
```pseudo
|
|
95
|
+
# BAD: Entire dataset in workflow state
|
|
96
|
+
workflow AnalyzeData(datasetId):
|
|
97
|
+
activity FetchDataset(datasetId) -> dataset # 500MB result stored in history
|
|
98
|
+
activity Analyze(dataset) -> results
|
|
99
|
+
|
|
100
|
+
# GOOD: Pass references, not data
|
|
101
|
+
workflow AnalyzeData(datasetId):
|
|
102
|
+
activity FetchAndStore(datasetId) -> dataRef # Returns S3 key, not data
|
|
103
|
+
activity Analyze(dataRef) -> results
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Why:** Every activity input and result is persisted in workflow history. Large payloads bloat history size, slow down replay, and may exceed Temporal's payload size limit. Pass references (IDs, URLs, keys) instead of data.
|
|
107
|
+
|
|
108
|
+
> **Default: defer to the payload codec.** Temporal supports a **payload codec / data converter** that transparently offloads, compresses, or encrypts large payloads with *no change* to workflow/activity signatures — and the claim-check pattern itself is, most of the time, best implemented as a codec server that swaps a large payload for a reference behind the scenes. So the decision is:
|
|
109
|
+
>
|
|
110
|
+
> - **Default — let the codec server handle it** (claim-check included). Note it cleanly and move on: *"large-payload claim-check handled by the codec server."* Do **not** invent a bespoke claim-check store at design time.
|
|
111
|
+
> - **Escalate to an explicit application-level `*Ref`** only when the data outlives the workflow, is shared across services, or needs an ownership/GC story the codec can't own. *Only in that case* does the design owe a one-line note on backing store + lifecycle.
|
|
112
|
+
|
|
113
|
+
## Primitive Misuse
|
|
114
|
+
|
|
115
|
+
### Signal for Request-Response
|
|
116
|
+
|
|
117
|
+
Using a signal when the caller needs confirmation or a return value.
|
|
118
|
+
|
|
119
|
+
```pseudo
|
|
120
|
+
# BAD: Signal has no return value — caller doesn't know if it worked
|
|
121
|
+
signal ApproveOrder(orderId):
|
|
122
|
+
approved = true
|
|
123
|
+
|
|
124
|
+
# GOOD: Update returns a result to the caller
|
|
125
|
+
update ApproveOrder(orderId: string) -> (ApprovalResult):
|
|
126
|
+
activity ValidateApproval(orderId) -> validation
|
|
127
|
+
if (validation.ok):
|
|
128
|
+
approved = true
|
|
129
|
+
return ApprovalResult{accepted: true}
|
|
130
|
+
else:
|
|
131
|
+
return ApprovalResult{accepted: false, reason: validation.error}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**Why:** Signals are fire-and-forget — the sender gets no acknowledgment, no validation, and no result. Use `update` when the caller needs to know the mutation was accepted.
|
|
135
|
+
|
|
136
|
+
### Query That Modifies State
|
|
137
|
+
|
|
138
|
+
Using a query handler to change workflow state.
|
|
139
|
+
|
|
140
|
+
```pseudo
|
|
141
|
+
# BAD: Query with side effects
|
|
142
|
+
query GetOrderStatus():
|
|
143
|
+
accessCount = accessCount + 1 # Modifies state!
|
|
144
|
+
return OrderStatus{status, accessCount}
|
|
145
|
+
|
|
146
|
+
# GOOD: Query is a pure read
|
|
147
|
+
query GetOrderStatus():
|
|
148
|
+
return OrderStatus{status}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Why:** Queries are read-only by contract. They may be called multiple times during replay without the workflow's knowledge. State modifications in queries produce unpredictable behavior and violate Temporal's execution model.
|
|
152
|
+
|
|
153
|
+
### Update Without Validation
|
|
154
|
+
|
|
155
|
+
Accepting an update without checking whether the mutation is valid.
|
|
156
|
+
|
|
157
|
+
```pseudo
|
|
158
|
+
# BAD: Blindly applies the update
|
|
159
|
+
update SetShippingAddress(address):
|
|
160
|
+
shippingAddress = address
|
|
161
|
+
return Result{ok: true}
|
|
162
|
+
|
|
163
|
+
# GOOD: Validate before committing
|
|
164
|
+
update SetShippingAddress(address: Address) -> (Result):
|
|
165
|
+
activity ValidateAddress(address) -> validation
|
|
166
|
+
if (validation.valid):
|
|
167
|
+
shippingAddress = address
|
|
168
|
+
return Result{ok: true}
|
|
169
|
+
else:
|
|
170
|
+
return Result{ok: false, error: validation.reason}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**Why:** Updates execute inside the workflow — invalid data corrupts workflow state. Always validate before committing. The caller receives the validation result, so they can react to rejection.
|
|
174
|
+
|
|
175
|
+
### Detach When You Need the Result
|
|
176
|
+
|
|
177
|
+
Using `detach` on a child workflow or nexus call when the parent needs the outcome.
|
|
178
|
+
|
|
179
|
+
```pseudo
|
|
180
|
+
# BAD: Detached — parent can't observe success or failure
|
|
181
|
+
detach workflow ProcessPayment(order)
|
|
182
|
+
# ... parent continues, has no idea if payment succeeded
|
|
183
|
+
|
|
184
|
+
# GOOD: Synchronous call or promise when result matters
|
|
185
|
+
workflow ProcessPayment(order) -> paymentResult
|
|
186
|
+
# or: promise p <- workflow ProcessPayment(order) ... await p -> paymentResult
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**Why:** `detach` is fire-and-forget — the parent cannot await the result, check for errors, or compensate on failure. Use `detach` only when you genuinely don't care about the outcome (audit logs, analytics, notifications where failure is acceptable).
|
|
190
|
+
|
|
191
|
+
## Activity Anti-Patterns
|
|
192
|
+
|
|
193
|
+
### Non-Determinism in Workflows
|
|
194
|
+
|
|
195
|
+
Using non-deterministic operations directly in workflow code.
|
|
196
|
+
|
|
197
|
+
```pseudo
|
|
198
|
+
# BAD: Current time varies on replay
|
|
199
|
+
# if (current_time() > deadline):
|
|
200
|
+
# cancel()
|
|
201
|
+
|
|
202
|
+
# BAD: Map iteration order varies across replays
|
|
203
|
+
# for (key in map.keys()):
|
|
204
|
+
# activity Process(key)
|
|
205
|
+
|
|
206
|
+
# BAD: Goroutines/threads — execution order not deterministic
|
|
207
|
+
# go func() { activity DoWork() }
|
|
208
|
+
|
|
209
|
+
# GOOD: Use Temporal primitives for time
|
|
210
|
+
# await one:
|
|
211
|
+
# activity DoWork() -> result:
|
|
212
|
+
# close complete(Result{result})
|
|
213
|
+
# timer(deadline):
|
|
214
|
+
# close fail(Result{status: "timeout"})
|
|
215
|
+
|
|
216
|
+
# GOOD: Sort before iterating
|
|
217
|
+
# for (key in sorted(map.keys())):
|
|
218
|
+
# activity Process(key)
|
|
219
|
+
|
|
220
|
+
# GOOD: Use promises for concurrency
|
|
221
|
+
# promise a <- activity DoWorkA()
|
|
222
|
+
# promise b <- activity DoWorkB()
|
|
223
|
+
# await a -> resultA
|
|
224
|
+
# await b -> resultB
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
**Why:** Temporal replays workflow code to reconstruct state. Any operation that produces different results on replay — time, random numbers, non-deterministic iteration, language-level threading — causes non-determinism errors. See [core-principles.md](./core-principles.md).
|
|
228
|
+
|
|
229
|
+
### Non-Idempotent Activities
|
|
230
|
+
|
|
231
|
+
Activities that fail or produce incorrect results on retry.
|
|
232
|
+
|
|
233
|
+
```pseudo
|
|
234
|
+
# BAD: Assumes fresh state — duplicate user on retry
|
|
235
|
+
activity CreateUser(name):
|
|
236
|
+
db.insert(User(name))
|
|
237
|
+
|
|
238
|
+
# GOOD: Create-or-get — idempotent
|
|
239
|
+
activity CreateUser(name):
|
|
240
|
+
existing = db.get_by_name(name)
|
|
241
|
+
if existing: return existing
|
|
242
|
+
return db.insert(User(name))
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
**Why:** Activities may be retried on network failures, worker crashes, or timeouts. An activity that isn't idempotent (same inputs → same result) will produce duplicate records, double charges, or inconsistent state. See [core-principles.md](./core-principles.md) for idempotency patterns.
|
|
246
|
+
|
|
247
|
+
### Orchestration in Activities
|
|
248
|
+
|
|
249
|
+
Putting multi-step logic, retry loops, or conditional branching inside an activity.
|
|
250
|
+
|
|
251
|
+
```pseudo
|
|
252
|
+
# BAD: Multi-step orchestration in activity — partial failure unrecoverable
|
|
253
|
+
activity DeployAll(specs):
|
|
254
|
+
for spec in specs:
|
|
255
|
+
deploy(spec) # If this fails on spec #5 of 10,
|
|
256
|
+
wait_healthy(spec) # specs 1-4 deployed but no rollback
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
```twf
|
|
260
|
+
# GOOD: Workflow orchestrates, each step independently retryable
|
|
261
|
+
workflow DeployAll(specs: Specs):
|
|
262
|
+
for (spec in specs.items):
|
|
263
|
+
activity Deploy(spec)
|
|
264
|
+
activity WaitHealthy(spec)
|
|
265
|
+
close complete
|
|
266
|
+
|
|
267
|
+
activity Deploy(spec: Spec):
|
|
268
|
+
deploy(spec)
|
|
269
|
+
|
|
270
|
+
activity WaitHealthy(spec: Spec):
|
|
271
|
+
wait_healthy(spec)
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**Why:** Activities run outside Temporal's durable execution model — if an activity fails mid-way through a loop, there's no replay, no history, and no way to resume from the last successful step. Workflows provide exactly this: durable, retryable orchestration with full visibility into progress.
|
|
275
|
+
|
|
276
|
+
### Activity Sprawl / Wrapping In-Memory Work
|
|
277
|
+
|
|
278
|
+
Wrapping work in an activity when nothing touches an external system — reading data the workflow already holds, in-memory derivation, or accumulation.
|
|
279
|
+
|
|
280
|
+
```pseudo
|
|
281
|
+
# BAD: activities that touch no external system
|
|
282
|
+
activity ReadCritiqueReady(state) -> ready # field access on held data
|
|
283
|
+
activity ListSubsetPaperIds(papers) -> ids # in-memory filter
|
|
284
|
+
activity AppendObservation(list, obs) -> list # building a collection
|
|
285
|
+
|
|
286
|
+
# GOOD: this is workflow code
|
|
287
|
+
ready = state.critiqueReady
|
|
288
|
+
ids = filter(papers, isSubset)
|
|
289
|
+
observations = append(observations, obs)
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
**Why:** Each spurious activity is a task-queue round-trip plus a history event for no resilience benefit. Activities are for I/O and side effects; in-memory work is deterministic workflow code. See [core-principles.md](./core-principles.md#activities-are-for-io--not-in-memory-work).
|
|
293
|
+
|
|
294
|
+
## Deployment Topology
|
|
295
|
+
|
|
296
|
+
### Nexus for Same-Namespace Calls
|
|
297
|
+
|
|
298
|
+
Using a Nexus operation to call a workflow that lives in the same namespace.
|
|
299
|
+
|
|
300
|
+
```pseudo
|
|
301
|
+
# BAD: Nexus hop within one namespace — adds an endpoint, a service
|
|
302
|
+
# contract, and latency for no boundary benefit
|
|
303
|
+
nexus InternalEndpoint InternalService.DoStep(args) -> result
|
|
304
|
+
|
|
305
|
+
# GOOD: same namespace — call the child workflow (or activity) directly
|
|
306
|
+
workflow DoStep(args) -> result
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
**Why:** Nexus exists to cross an **organizational** boundary — different team, security context, deployment lifecycle, or an external service contract. Within a single namespace those boundaries don't exist, so Nexus only adds an endpoint declaration, a typed contract, and a network hop. Coupling between workflows is an argument for *co-location*, not a Nexus boundary. See [namespaces.md](./namespaces.md) and [workflow-boundaries.md](./workflow-boundaries.md#use-nexus-when).
|
|
310
|
+
|
|
311
|
+
### Deployment Config in Workers Instead of Namespaces
|
|
312
|
+
|
|
313
|
+
Putting task queues, concurrency limits, or other deployment options on `worker` definitions.
|
|
314
|
+
|
|
315
|
+
```pseudo
|
|
316
|
+
# BAD: worker carries deployment config
|
|
317
|
+
worker orderTypes:
|
|
318
|
+
workflow ProcessOrder
|
|
319
|
+
options:
|
|
320
|
+
task_queue: "orders" # workers are type sets, not deployments
|
|
321
|
+
|
|
322
|
+
# GOOD: worker is a reusable type set; the namespace instantiates it with config
|
|
323
|
+
worker orderTypes:
|
|
324
|
+
workflow ProcessOrder
|
|
325
|
+
|
|
326
|
+
namespace ecommerce:
|
|
327
|
+
worker orderTypes
|
|
328
|
+
options:
|
|
329
|
+
task_queue: "orders"
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
**Why:** A `worker` is a *reusable type set* (which workflows/activities/services run together) with no deployment config; the `namespace` is what instantiates it with a `task_queue` and options. Mixing the two prevents reusing the same type set across namespaces (staging vs prod) and is rejected by `twf check`. See [task-queues.md](../topics/task-queues.md).
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Common Errors
|
|
2
|
+
|
|
3
|
+
This file covers **parser, resolver, and validator diagnostics** emitted by
|
|
4
|
+
`twf check` and `twf parse`. For **design-level anti-patterns** (structural
|
|
5
|
+
mistakes, primitive misuse), see [anti-patterns.md](./anti-patterns.md).
|
|
6
|
+
|
|
7
|
+
Each row lists the symbolic `code` (stable across releases), the human
|
|
8
|
+
message you'll see, the cause, and the fix. The codes are also emitted by
|
|
9
|
+
`twf parse` inside the structured envelope (`diagnostics[].code`); programmatic
|
|
10
|
+
consumers should match on `kind+code` rather than the message.
|
|
11
|
+
|
|
12
|
+
## Resolve errors (kind: `resolve`)
|
|
13
|
+
|
|
14
|
+
| Code | Message | Cause | Fix |
|
|
15
|
+
|------|---------|-------|-----|
|
|
16
|
+
| `UNDEFINED_ACTIVITY` | `undefined activity: Foo` | Activity `Foo` is called but not defined | Add `activity Foo(...):` definition to the file |
|
|
17
|
+
| `UNDEFINED_WORKFLOW` | `undefined workflow: Foo` | Child workflow `Foo` is called but not defined | Add `workflow Foo(...):` definition to the file |
|
|
18
|
+
| `UNDEFINED_SIGNAL` | `undefined signal: Foo` | `await signal Foo` or `signal Foo:` case but no signal handler declared | Add `signal Foo(...):` declaration inside the workflow, before the body |
|
|
19
|
+
| `UNDEFINED_UPDATE` | `undefined update: Foo` | `await update Foo` or `update Foo:` case but no update handler declared | Add `update Foo(...) -> (Type):` declaration inside the workflow, before the body |
|
|
20
|
+
| `UNDEFINED_CONDITION` | `undefined condition: Foo` | `set Foo`, `unset Foo`, or `await Foo` but no condition declared | Add `condition Foo` inside the workflow's `state:` block |
|
|
21
|
+
| `UNDEFINED_PROMISE_OR_CONDITION` | `undefined promise or condition: Foo` | `await Foo` or `Foo:` case in `await one` but `Foo` is not a promise or condition | Add `promise Foo <- ...` in the workflow body or `condition Foo` in the `state:` block |
|
|
22
|
+
| `DUPLICATE_WORKFLOW` | `duplicate workflow definition: Foo` | Two `workflow Foo` definitions in the same file | Remove or rename the duplicate |
|
|
23
|
+
| `DUPLICATE_ACTIVITY` | `duplicate activity definition: Foo` | Two `activity Foo` definitions in the same file | Remove or rename the duplicate |
|
|
24
|
+
| `DUPLICATE_WORKER` | `duplicate worker definition: Foo` | Two `worker Foo` definitions | Remove or rename the duplicate |
|
|
25
|
+
| `DUPLICATE_NAMESPACE` | `duplicate namespace definition: Foo` | Two `namespace Foo` blocks | Remove or rename the duplicate |
|
|
26
|
+
| `DUPLICATE_NEXUS_SERVICE` | `duplicate nexus service definition: Foo` | Two `nexus service Foo` blocks | Remove or rename the duplicate |
|
|
27
|
+
| `DUPLICATE_ENDPOINT` | `duplicate nexus endpoint name "Foo": defined in namespace A and namespace B` | Same endpoint name in multiple namespaces | Use unique endpoint names |
|
|
28
|
+
| `CONDITION_RESULT_BINDING` | `condition "Foo" cannot have a result binding (-> identifier)` | `await Foo -> result` where `Foo` is a condition | Conditions are boolean — remove the `-> result` binding |
|
|
29
|
+
| `NEXUS_ASYNC_UNDEFINED_WORKFLOW` | `async operation Foo references undefined workflow: Bar` | Async nexus op points at a workflow that doesn't exist | Add the workflow or fix the name |
|
|
30
|
+
| `NEXUS_UNDEFINED_ENDPOINT` | `undefined nexus endpoint: Foo` | Endpoint referenced but not defined anywhere | Add a `nexus endpoint Foo:` in some namespace, or fix the name |
|
|
31
|
+
| `NEXUS_UNDEFINED_SERVICE` | `undefined nexus service: Foo` | Service referenced but not defined | Add a `nexus service Foo:` block or fix the name |
|
|
32
|
+
| `NEXUS_NO_OPERATION` | `nexus service Foo has no operation Bar` | Operation name not in the service | Add the operation or fix the name |
|
|
33
|
+
| `WORKER_UNDEFINED_WORKFLOW` / `WORKER_UNDEFINED_ACTIVITY` / `WORKER_UNDEFINED_NEXUS_SERVICE` | `worker X references undefined ...` | Worker lists a name that doesn't exist | Add the definition or fix the name |
|
|
34
|
+
| `NAMESPACE_UNDEFINED_WORKER` | `namespace X references undefined worker: Y` | Namespace uses unknown worker | Add worker block or fix name |
|
|
35
|
+
|
|
36
|
+
### Nexus resolution: external (warning) vs. local (error)
|
|
37
|
+
|
|
38
|
+
Nexus references resolve in one of two modes, decided **per category** — services and
|
|
39
|
+
endpoints are independent axes:
|
|
40
|
+
|
|
41
|
+
| Category | Nothing of that category defined in the file set | Any of that category defined |
|
|
42
|
+
|----------|--------------------------------------------------|------------------------------|
|
|
43
|
+
| Service | `NEXUS_UNRESOLVED_SERVICE` — warning, exit 0 ("may be external") | `NEXUS_UNDEFINED_SERVICE` — error, exit 1 |
|
|
44
|
+
| Endpoint | `NEXUS_UNRESOLVED_ENDPOINT` — warning, exit 0 ("may be external") | `NEXUS_UNDEFINED_ENDPOINT` — error, exit 1 |
|
|
45
|
+
|
|
46
|
+
(Endpoints are defined inside `namespace` blocks; services via top-level `nexus service`.)
|
|
47
|
+
|
|
48
|
+
**Gotcha:** defining *one* local service retroactively turns *every other* service reference
|
|
49
|
+
into a hard error — even references to genuinely external services in other namespaces. This is
|
|
50
|
+
a sharp cliff for a partial / per-package file that both *calls* external services and *provides*
|
|
51
|
+
its own. Until an explicit external marker exists, either (a) add a local stub definition for
|
|
52
|
+
each external service you call, or (b) define no nexus services in the file and accept the
|
|
53
|
+
warnings.
|
|
54
|
+
|
|
55
|
+
## Parse errors (kind: `parse`)
|
|
56
|
+
|
|
57
|
+
All parse failures share the single code `SYNTAX`. The message carries the
|
|
58
|
+
detail; pin programmatic dispatch to `kind=parse, code=SYNTAX` and match on
|
|
59
|
+
the message for now (categorical parse codes are future work).
|
|
60
|
+
|
|
61
|
+
| Message | Cause | Fix |
|
|
62
|
+
|---------|-------|-----|
|
|
63
|
+
| `<keyword> is not allowed in activity body` | Using a temporal primitive (`workflow`, `activity`, `timer`, `signal`, `await`, etc.) inside an activity definition or query handler | Move the temporal primitive to a workflow. Activities run outside the replay-safe workflow context as normal side-effecting code — temporal primitives require deterministic replay and cannot function in activities. |
|
|
64
|
+
| `expected ( after return type ->` | Return type not parenthesized: `-> Result` | Use `-> (Result)` — return types must be wrapped in parentheses |
|
|
65
|
+
| `expected ( after if` / `expected ( after for` | Missing parentheses around condition/iterator | Use `if (expr):` / `for (x in items):` |
|
|
66
|
+
| `unexpected token <tok> at top level` | Statement or keyword that doesn't start a workflow or activity definition | Ensure all top-level items are `workflow`, `activity`, `worker`, `namespace`, or `nexus service` definitions |
|
|
67
|
+
| `unexpected token <tok> in await one case` | Invalid case type inside `await one:` block | Cases must be `signal`, `update`, `timer`, `activity`, `workflow`, an identifier, or `await all` |
|
|
68
|
+
| `expected COLON, got NEWLINE` | A definition is missing its `:` and indented body — a bare declaration like `activity Foo(x) -> (R)` with nothing under it. `activity`/`workflow`/`sync` nexus op definitions always require a body. (Often followed by a cascading `UNDEFINED_*` because the malformed definition didn't register.) | Add `:` and an indented body. For a not-yet-implemented stub, use a placeholder statement (e.g. `return Foo{}` or a single `log(...)`); a definition cannot be body-less. |
|
|
69
|
+
|
|
70
|
+
## Validation diagnostics (kind: `validate`)
|
|
71
|
+
|
|
72
|
+
| Code | Severity | Cause | Fix |
|
|
73
|
+
|------|----------|-------|-----|
|
|
74
|
+
| `MISSING_TASK_QUEUE` | error | Worker instantiation has no `task_queue` option | Add `options: task_queue: "..."` to the worker instantiation |
|
|
75
|
+
| `MISSING_ENDPOINT_TASK_QUEUE` | error | Nexus endpoint instantiation has no `task_queue` | Add the option to the endpoint instantiation |
|
|
76
|
+
| `EXPLICIT_ROUTING_MISMATCH` | error | An activity/workflow call's explicit `task_queue` doesn't match any worker registering it | Fix the queue name or register the target on a worker for that queue |
|
|
77
|
+
| `IMPLICIT_ROUTING_MISMATCH` | error | An activity/workflow is called without an explicit `task_queue` and no worker on the caller's queue registers it | Add the target to a worker on the same queue, or pass an explicit `task_queue` option |
|
|
78
|
+
| `ENDPOINT_SERVICE_LINKAGE` | error | Endpoint routes to a task queue but no worker on that queue registers the service | Register the service on a worker for the endpoint's queue |
|
|
79
|
+
| `TASK_QUEUE_MISMATCH` | error | Two workers share a queue but register different type sets | Make the type sets identical, or use distinct queues |
|
|
80
|
+
| `TASK_QUEUE_IDENTICAL` | warning | Two workers register identical type sets on the same queue (redundant) | Drop one of the workers |
|
|
81
|
+
| `UNCOVERED_WORKFLOW` / `UNCOVERED_ACTIVITY` / `UNCOVERED_SERVICE` | warning | Definition exists but no instantiated worker registers it | Register on a worker or remove the unused definition |
|
|
82
|
+
| `UNINSTANTIATED_WORKER` | warning | Worker defined but never instantiated in any namespace | Instantiate it in a namespace, or remove the worker |
|
|
83
|
+
| `EMPTY_WORKFLOW` / `EMPTY_ACTIVITY` / `EMPTY_WORKER` / `EMPTY_NAMESPACE` | warning | Block has no body / no registrations / no instantiations | Add content or remove the empty block |
|
|
84
|
+
|
|
85
|
+
The diagnostic shape is the `envelope.Diagnostic` Go struct
|
|
86
|
+
(`tools/lsp/cmd/twf/internal/envelope/model.go`); run any `twf --json`
|
|
87
|
+
subcommand to see it live, or read its TypeScript projection in
|
|
88
|
+
`tools/wire-types`.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Determinism & Idempotency
|
|
2
|
+
|
|
3
|
+
## Determinism: Workflows Must Replay Identically
|
|
4
|
+
|
|
5
|
+
Temporal replays workflow code to reconstruct state. Different replay results = non-determinism errors. See [Temporal: Deterministic Constraints](https://docs.temporal.io/workflows#deterministic-constraints) for the authoritative reference.
|
|
6
|
+
|
|
7
|
+
| Safe in Workflows | Must Be in Activities |
|
|
8
|
+
|-------------------|----------------------|
|
|
9
|
+
| Logic on activity results | Current time, dates |
|
|
10
|
+
| Deterministic loops/conditionals | Random numbers, UUIDs |
|
|
11
|
+
| Child workflows | HTTP/API calls |
|
|
12
|
+
| Temporal timers | Database operations |
|
|
13
|
+
| Local variables | File I/O |
|
|
14
|
+
| Signal waits | External service calls |
|
|
15
|
+
| Deterministic iteration (arrays, slices) | Map/dictionary iteration (order varies) |
|
|
16
|
+
| Temporal SDK concurrency (promises, await all) | Language-level threads, goroutines, async |
|
|
17
|
+
| Workflow-local state | Mutable global/shared state |
|
|
18
|
+
|
|
19
|
+
**Workflows = pure orchestration. Activities = side effects.**
|
|
20
|
+
|
|
21
|
+
### Activities Are for I/O — Not In-Memory Work
|
|
22
|
+
|
|
23
|
+
The table above is often *over*-applied into activity sprawl: wrapping things in an activity that were never side effects. **Activities are for I/O and side effects. Do not wrap in an activity:**
|
|
24
|
+
|
|
25
|
+
- **Reads of data the workflow already holds** — field access, lookups into a struct/ref passed in or returned by an earlier activity (`ReadCritiqueReady`, `LookupBundleRef`). The workflow already has the data; reading it is workflow code.
|
|
26
|
+
- **In-memory derivation** — filtering, mapping, computing a value from inputs the workflow holds (`ListSubsetPaperIds`).
|
|
27
|
+
- **Accumulation** — appending to a list or building up state (`AppendObservations`, `AppendTrajectory`). Building a collection is deterministic in-memory work and belongs in the workflow body (expressible directly, including as a raw statement) — there is no need for an `Append*` activity.
|
|
28
|
+
|
|
29
|
+
Each spurious activity is a task-queue round-trip plus a history event for **no resilience benefit**. The litmus test: *does it touch an external system or produce a side effect?* If not, it's workflow code.
|
|
30
|
+
|
|
31
|
+
> **Optimization (away from the default):** batch several small calls into one activity only when they always succeed/fail together and per-call retry isn't meaningful; consider local activities for short deterministic helpers. These are deviations from "one activity per network call" (see [workflow-boundaries.md](./workflow-boundaries.md)), not the starting point.
|
|
32
|
+
|
|
33
|
+
## Idempotency: Activities May Run Multiple Times
|
|
34
|
+
|
|
35
|
+
Retries happen (network failures, crashes, timeouts). Activities must be **idempotent**: same inputs → same result regardless of execution count.
|
|
36
|
+
|
|
37
|
+
| Pattern | Example |
|
|
38
|
+
|---------|---------|
|
|
39
|
+
| **Create-or-get** — when entity has a natural unique key | Check existence before creating |
|
|
40
|
+
| **Idempotency keys** — when external system supports them | Workflow ID + activity name as operation key |
|
|
41
|
+
| **Upsert** — when database supports atomic upsert | Prefer over insert-then-update |
|
|
42
|
+
| **Deduplication** — last resort when no built-in mechanism | Query before mutating |
|
|
43
|
+
|
|
44
|
+
**Think through retries:** CreateUser → return existing if exists. SendEmail → provider idempotency key. DeployResource → verify state, return success if deployed.
|
|
45
|
+
|
|
46
|
+
### State the Strategy in the Design
|
|
47
|
+
|
|
48
|
+
Knowing the patterns isn't enough — the *design* must **state** which one each side-effecting activity uses, so idempotency is a load-bearing decision rather than an assumed prose comment. For every activity that isn't idempotent by nature, name its strategy and key derivation, e.g.:
|
|
49
|
+
|
|
50
|
+
> `ChargePayment` — idempotency key = `"{workflow_id}-ChargePayment"`; provider dedupes on it.
|
|
51
|
+
|
|
52
|
+
This is a **skill/design concern, not a `twf check` rule** — Temporal has no call-site `idempotency_key` option, so the parser cannot validate it. The [Design Review](../SKILL.md#design-review) checks that each non-idempotent activity carries this note.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Design Checklist
|
|
2
|
+
|
|
3
|
+
**Validation ≠ review.** *Validation* asks "does it parse and resolve?" (`twf check`). *Review* asks "is it a good Temporal design?" — and no tool answers that. A clean `twf check` clears only the first group below; the [Design Review](#design-review) group is where design quality lives. Don't present on a green tool alone.
|
|
4
|
+
|
|
5
|
+
## TWF Validation
|
|
6
|
+
- [ ] `twf check` passes (`✓ OK`)
|
|
7
|
+
- [ ] `twf symbols` lists all expected definitions
|
|
8
|
+
- [ ] No undefined references
|
|
9
|
+
- [ ] No SDK-specific code in `.twf`
|
|
10
|
+
→ See [common-errors.md](./common-errors.md) for error troubleshooting
|
|
11
|
+
|
|
12
|
+
## Design Review (fresh-eyes pass — no tool catches these)
|
|
13
|
+
- [ ] **Call-site integrity** — every activity/workflow/nexus definition has a *structured* call site (no orphaned `x = Name(args)` parsing as `raw`)
|
|
14
|
+
- [ ] **Reachability** — every workflow is reachable from a declared entry point; no dead workflows
|
|
15
|
+
- [ ] Design re-checked against **every** anti-pattern in [anti-patterns.md](./anti-patterns.md)
|
|
16
|
+
- [ ] Each non-idempotent activity names its idempotency strategy + key derivation
|
|
17
|
+
- [ ] Concurrent fan-out branches that write shared external state state their isolation/keying assumption
|
|
18
|
+
→ See [SKILL.md § Design Review](../SKILL.md#design-review)
|
|
19
|
+
|
|
20
|
+
## Determinism
|
|
21
|
+
- [ ] All I/O, time, randomness in activities
|
|
22
|
+
- [ ] No external calls in workflow code
|
|
23
|
+
- [ ] Loops have deterministic bounds
|
|
24
|
+
- [ ] Timers use Temporal primitives
|
|
25
|
+
- [ ] No non-deterministic data structure iteration (maps, sets)
|
|
26
|
+
- [ ] Version-specific branching uses proper versioning pattern
|
|
27
|
+
→ See [core-principles.md](./core-principles.md) for determinism rules
|
|
28
|
+
|
|
29
|
+
## Idempotency
|
|
30
|
+
- [ ] Activities handle "already exists" gracefully
|
|
31
|
+
- [ ] Retries produce same end state
|
|
32
|
+
- [ ] No duplicate side effects on replay
|
|
33
|
+
→ See [core-principles.md](./core-principles.md) for idempotency patterns
|
|
34
|
+
|
|
35
|
+
## Failure Handling
|
|
36
|
+
- [ ] Each failure mode identified
|
|
37
|
+
- [ ] Recovery strategy defined (retry, compensate, fail)
|
|
38
|
+
- [ ] Partial success handled
|
|
39
|
+
- [ ] Timeouts configured
|
|
40
|
+
→ See [anti-patterns.md](./anti-patterns.md) for common failure handling mistakes
|
|
41
|
+
|
|
42
|
+
## Runtime, Cost & Lifecycle
|
|
43
|
+
- [ ] Loops with large accumulated history reach `close continue_as_new` (bound alone is not enough); strategy stated
|
|
44
|
+
- [ ] Large payloads: either deferred to the data converter/codec, or (if an explicit claim-check `*Ref`) a one-line store + lifecycle note
|
|
45
|
+
→ See [anti-patterns.md](./anti-patterns.md#large-payloads-in-workflow-state) and [long-running.md](../topics/long-running.md)
|
|
46
|
+
|
|
47
|
+
## Decomposition
|
|
48
|
+
- [ ] Each workflow has single clear purpose
|
|
49
|
+
- [ ] Child workflow vs activity choice justified
|
|
50
|
+
- [ ] Workflow names describe outcomes, not steps
|
|
51
|
+
→ See [workflow-boundaries.md](./workflow-boundaries.md) for boundary decisions
|
|
52
|
+
|
|
53
|
+
## Deployment Topology (design review — `twf check` validates syntax)
|
|
54
|
+
- [ ] Worker groupings reflect actual deployment needs (not just "one worker for everything")
|
|
55
|
+
- [ ] Task queue separation matches scaling and isolation requirements
|
|
56
|
+
- [ ] Namespace count justified by org / security / lifecycle / external-contract boundaries (default: one)
|
|
57
|
+
- [ ] Cross-namespace calls have nexus endpoints
|
|
58
|
+
- [ ] `twf check` passes topology validation
|
|
59
|
+
→ See [task-queues.md](../topics/task-queues.md) for task queue design and [namespaces.md](./namespaces.md) for namespace count
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Namespaces: How Many?
|
|
2
|
+
|
|
3
|
+
How many namespaces should a design use? This is a cross-cutting deployment decision — it interacts with workers, task queues, and Nexus — so it lives here as a boundary call alongside [workflow-boundaries.md](./workflow-boundaries.md), not as a construct deep-dive. The load-bearing rule:
|
|
4
|
+
|
|
5
|
+
> **Namespaces are organizational, not architectural.** They are an operational/ownership boundary, not a decomposition tool. The default is **one**; adding more requires justification.
|
|
6
|
+
|
|
7
|
+
Without this rule, designs drift toward "one namespace per layer → one per worker," which is almost always an overuse. Agent/tool scoping is solved by worker registration; runtime heterogeneity by task queues; layer separation by workflow boundaries. **None of those justify a namespace.**
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Decision Ladder
|
|
12
|
+
|
|
13
|
+
### Default: one namespace
|
|
14
|
+
|
|
15
|
+
Start here. A single namespace holds all your workers, workflows, and activities. Tightly-coupled workflows belong **together** — coupling is an argument for co-location, not separation.
|
|
16
|
+
|
|
17
|
+
### Add a namespace only when one of these demands it
|
|
18
|
+
|
|
19
|
+
| Reason | Why it's a namespace boundary |
|
|
20
|
+
|--------|-------------------------------|
|
|
21
|
+
| **Distinct team owns the workflows** | Separate ownership, access control, on-call |
|
|
22
|
+
| **Different security / compliance context** | PCI vs non-PCI, tenant isolation at the org level |
|
|
23
|
+
| **Independent deployment lifecycle** | Separate release cadence and blast radius |
|
|
24
|
+
| **External service contract across an org boundary** | The Nexus case — a typed contract between services |
|
|
25
|
+
|
|
26
|
+
### Explicitly NOT reasons to add a namespace
|
|
27
|
+
|
|
28
|
+
| Drift | What actually solves it |
|
|
29
|
+
|-------|-------------------------|
|
|
30
|
+
| Different worker / runtime (GPU, licensed software) | **Task queues** ([task-queues.md](../topics/task-queues.md)) |
|
|
31
|
+
| Agent or tool scoping | **Worker registration** (which types run together) |
|
|
32
|
+
| Layer separation (inner vs outer logic) | **Workflow boundaries** (child workflows / activities) |
|
|
33
|
+
| "One per worker" by default | Nothing — co-locate them |
|
|
34
|
+
| "It feels cleaner" | Nothing — resist it |
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Worked Judgment: Two-Layer Agent System
|
|
39
|
+
|
|
40
|
+
Consider an inner agent (executes tools) and an outer agent (plans, calls the inner agent), each with its own tools.
|
|
41
|
+
|
|
42
|
+
- **Tempting (wrong) start:** a namespace per worker — one for the planner, one for each tool runner — arriving at 5-6 namespaces.
|
|
43
|
+
- **Why it's wrong:** inner-agent vs outer-agent *tool scoping* is **worker registration** (register each agent's tools on its own worker), not a namespace boundary. The layers are tightly coupled, which argues for co-location.
|
|
44
|
+
- **The legitimate split:** the only real boundary is the **org/service contract** between the two layers — if the inner agent is genuinely an independent service with its own contract, that justifies **two** namespaces connected by **Nexus**. Not more.
|
|
45
|
+
|
|
46
|
+
The mechanism is **worker registration for scoping, namespaces only for the service boundary**:
|
|
47
|
+
|
|
48
|
+
```twf
|
|
49
|
+
# Tool scoping is worker registration, NOT a namespace:
|
|
50
|
+
worker outerAgentWorker:
|
|
51
|
+
workflow OuterAgent
|
|
52
|
+
activity PlanSteps
|
|
53
|
+
activity SummarizeOutcome
|
|
54
|
+
|
|
55
|
+
worker innerAgentWorker:
|
|
56
|
+
workflow InnerAgent
|
|
57
|
+
activity SearchTool
|
|
58
|
+
activity CalcTool
|
|
59
|
+
nexus service InnerAgentService
|
|
60
|
+
|
|
61
|
+
# Exactly two namespaces: one per org/service-contract boundary.
|
|
62
|
+
namespace outerAgent:
|
|
63
|
+
worker outerAgentWorker
|
|
64
|
+
options:
|
|
65
|
+
task_queue: "outer-agent"
|
|
66
|
+
|
|
67
|
+
namespace innerAgent:
|
|
68
|
+
worker innerAgentWorker
|
|
69
|
+
options:
|
|
70
|
+
task_queue: "inner-agent"
|
|
71
|
+
nexus endpoint InnerAgentEndpoint
|
|
72
|
+
options:
|
|
73
|
+
task_queue: "inner-agent"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The final two-namespaces-with-Nexus topology can be a fine outcome; the mistake is *starting* at one-per-worker and being talked back down. Start at one, and require a reason from the ladder above to add each additional namespace.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Related
|
|
81
|
+
|
|
82
|
+
- [task-queues.md](../topics/task-queues.md) — different runtimes use task queues, not namespaces.
|
|
83
|
+
- [nexus.md](../topics/nexus.md) — the cross-namespace contract; the one mechanism that legitimately spans namespaces.
|
|
84
|
+
- [workflow-boundaries.md](./workflow-boundaries.md) — child workflow vs activity vs nexus.
|