@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,213 @@
|
|
|
1
|
+
# Source: promises-conditions.md
|
|
2
|
+
# Patterns: promise declarations, conditions, state blocks, set/unset
|
|
3
|
+
|
|
4
|
+
# --- Start-now, wait-later (promises with activities) ---
|
|
5
|
+
|
|
6
|
+
workflow ParallelProcessing(items: Items) -> (Result):
|
|
7
|
+
# Start two activities without blocking
|
|
8
|
+
promise handleA <- activity ProcessA(items.a)
|
|
9
|
+
promise handleB <- activity ProcessB(items.b)
|
|
10
|
+
|
|
11
|
+
# Do other work while they run
|
|
12
|
+
activity QuickSetup(items)
|
|
13
|
+
|
|
14
|
+
# Collect results
|
|
15
|
+
await handleA -> resultA
|
|
16
|
+
await handleB -> resultB
|
|
17
|
+
|
|
18
|
+
close complete(Result{a: resultA, b: resultB})
|
|
19
|
+
|
|
20
|
+
# --- Promise with child workflow ---
|
|
21
|
+
|
|
22
|
+
workflow WorkflowWithAsyncChild(input: Input) -> (Result):
|
|
23
|
+
# Start child workflow without blocking
|
|
24
|
+
promise childHandle <- workflow SlowChild(input.data)
|
|
25
|
+
|
|
26
|
+
# Do other work
|
|
27
|
+
activity QuickTask(input)
|
|
28
|
+
|
|
29
|
+
# Wait for child result
|
|
30
|
+
await childHandle -> childResult
|
|
31
|
+
|
|
32
|
+
close complete(Result{childResult})
|
|
33
|
+
|
|
34
|
+
# --- Promise with timer ---
|
|
35
|
+
|
|
36
|
+
workflow TimedOperation(data: Data) -> (Result):
|
|
37
|
+
promise timeout <- timer(5m)
|
|
38
|
+
|
|
39
|
+
promise work <- activity LongProcess(data)
|
|
40
|
+
|
|
41
|
+
# Race: process finishes or timeout fires
|
|
42
|
+
await one:
|
|
43
|
+
work -> result:
|
|
44
|
+
close complete(Result{data: result})
|
|
45
|
+
timeout:
|
|
46
|
+
close fail(Result{error: "timed out"})
|
|
47
|
+
|
|
48
|
+
# --- Update handler waits on condition ---
|
|
49
|
+
|
|
50
|
+
workflow ClusterManager(config: Config):
|
|
51
|
+
state:
|
|
52
|
+
condition clusterStarted
|
|
53
|
+
|
|
54
|
+
signal Shutdown():
|
|
55
|
+
shutdownRequested = true
|
|
56
|
+
|
|
57
|
+
update WaitUntilStarted() -> (ClusterState):
|
|
58
|
+
await clusterStarted
|
|
59
|
+
return ClusterState{started: true}
|
|
60
|
+
|
|
61
|
+
query GetStatus() -> (string):
|
|
62
|
+
return status
|
|
63
|
+
|
|
64
|
+
status = "provisioning"
|
|
65
|
+
activity ProvisionCluster(config)
|
|
66
|
+
activity StartCluster(config)
|
|
67
|
+
set clusterStarted
|
|
68
|
+
status = "running"
|
|
69
|
+
|
|
70
|
+
await signal Shutdown
|
|
71
|
+
close complete
|
|
72
|
+
|
|
73
|
+
# --- Condition racing against timer ---
|
|
74
|
+
|
|
75
|
+
workflow DepositCollector(accountId: string) -> (CollectionResult):
|
|
76
|
+
state:
|
|
77
|
+
condition thresholdReached
|
|
78
|
+
|
|
79
|
+
signal Deposit(amount: decimal):
|
|
80
|
+
balance = balance + amount
|
|
81
|
+
if (balance >= 1000):
|
|
82
|
+
set thresholdReached
|
|
83
|
+
|
|
84
|
+
balance = 0
|
|
85
|
+
|
|
86
|
+
await one:
|
|
87
|
+
thresholdReached:
|
|
88
|
+
close complete(CollectionResult{status: "threshold_reached", balance: balance})
|
|
89
|
+
timer(30d):
|
|
90
|
+
close complete(CollectionResult{status: "timeout", balance: balance})
|
|
91
|
+
|
|
92
|
+
# --- Promise racing in await one ---
|
|
93
|
+
|
|
94
|
+
workflow ResilientProcess(data: Data) -> (Result):
|
|
95
|
+
signal Cancel():
|
|
96
|
+
cancelRequested = true
|
|
97
|
+
|
|
98
|
+
promise handle <- activity LongProcess(data)
|
|
99
|
+
|
|
100
|
+
activity PrepareOutput(data)
|
|
101
|
+
|
|
102
|
+
await one:
|
|
103
|
+
handle -> result:
|
|
104
|
+
close complete(Result{data: result})
|
|
105
|
+
signal Cancel:
|
|
106
|
+
close fail(Result{error: "cancelled"})
|
|
107
|
+
|
|
108
|
+
# --- Promise with nexus workflow ---
|
|
109
|
+
|
|
110
|
+
workflow CrossNamespaceAsync(payment: Payment) -> (Result):
|
|
111
|
+
# Start nexus call without blocking
|
|
112
|
+
promise payHandle <- nexus BillingEndpoint BillingService.ChargePayment(payment)
|
|
113
|
+
|
|
114
|
+
# Do local work
|
|
115
|
+
activity PrepareReceipt(payment)
|
|
116
|
+
|
|
117
|
+
# Wait for nexus result
|
|
118
|
+
await payHandle -> paymentResult
|
|
119
|
+
|
|
120
|
+
close complete(Result{paymentId: paymentResult.id})
|
|
121
|
+
|
|
122
|
+
# --- Supporting activity definitions ---
|
|
123
|
+
|
|
124
|
+
activity ProcessA(data: Data) -> (ResultA):
|
|
125
|
+
return processA(data)
|
|
126
|
+
|
|
127
|
+
activity ProcessB(data: Data) -> (ResultB):
|
|
128
|
+
return processB(data)
|
|
129
|
+
|
|
130
|
+
activity QuickSetup(items: Items):
|
|
131
|
+
setup(items)
|
|
132
|
+
|
|
133
|
+
activity QuickTask(input: Input):
|
|
134
|
+
quick(input)
|
|
135
|
+
|
|
136
|
+
activity LongProcess(data: Data) -> (ProcessResult):
|
|
137
|
+
return longProcess(data)
|
|
138
|
+
|
|
139
|
+
activity PrepareOutput(data: Data):
|
|
140
|
+
prepare(data)
|
|
141
|
+
|
|
142
|
+
activity ProvisionCluster(config: Config):
|
|
143
|
+
provision(config)
|
|
144
|
+
|
|
145
|
+
activity StartCluster(config: Config):
|
|
146
|
+
start(config)
|
|
147
|
+
|
|
148
|
+
activity PrepareReceipt(payment: Payment):
|
|
149
|
+
prepare(payment)
|
|
150
|
+
|
|
151
|
+
# --- Supporting workflow definitions ---
|
|
152
|
+
|
|
153
|
+
workflow SlowChild(data: Data) -> (ChildResult):
|
|
154
|
+
activity DoSlowWork(data) -> result
|
|
155
|
+
close complete(ChildResult{result})
|
|
156
|
+
|
|
157
|
+
activity DoSlowWork(data: Data) -> (WorkResult):
|
|
158
|
+
return slowWork(data)
|
|
159
|
+
|
|
160
|
+
workflow BillingChargeWorkflow(payment: Payment) -> (PaymentResult):
|
|
161
|
+
activity ChargeCard(payment) -> result
|
|
162
|
+
close complete(PaymentResult{id: result.id})
|
|
163
|
+
|
|
164
|
+
activity ChargeCard(payment: Payment) -> (ChargeResult):
|
|
165
|
+
return charge(payment)
|
|
166
|
+
|
|
167
|
+
# --- Nexus service definition ---
|
|
168
|
+
|
|
169
|
+
nexus service BillingService:
|
|
170
|
+
async ChargePayment workflow BillingChargeWorkflow
|
|
171
|
+
|
|
172
|
+
# --- Worker and namespace definitions ---
|
|
173
|
+
|
|
174
|
+
worker promisesWorker:
|
|
175
|
+
workflow ParallelProcessing
|
|
176
|
+
workflow WorkflowWithAsyncChild
|
|
177
|
+
workflow TimedOperation
|
|
178
|
+
workflow ClusterManager
|
|
179
|
+
workflow DepositCollector
|
|
180
|
+
workflow ResilientProcess
|
|
181
|
+
workflow CrossNamespaceAsync
|
|
182
|
+
workflow SlowChild
|
|
183
|
+
activity ProcessA
|
|
184
|
+
activity ProcessB
|
|
185
|
+
activity QuickSetup
|
|
186
|
+
activity QuickTask
|
|
187
|
+
activity LongProcess
|
|
188
|
+
activity PrepareOutput
|
|
189
|
+
activity ProvisionCluster
|
|
190
|
+
activity StartCluster
|
|
191
|
+
activity PrepareReceipt
|
|
192
|
+
activity DoSlowWork
|
|
193
|
+
|
|
194
|
+
worker billingWorker:
|
|
195
|
+
workflow BillingChargeWorkflow
|
|
196
|
+
activity ChargeCard
|
|
197
|
+
nexus service BillingService
|
|
198
|
+
|
|
199
|
+
# Two namespaces: billing owns the service and exposes the endpoint;
|
|
200
|
+
# promises is the caller — CrossNamespaceAsync reaches across the boundary.
|
|
201
|
+
|
|
202
|
+
namespace billing:
|
|
203
|
+
worker billingWorker
|
|
204
|
+
options:
|
|
205
|
+
task_queue: "billing"
|
|
206
|
+
nexus endpoint BillingEndpoint
|
|
207
|
+
options:
|
|
208
|
+
task_queue: "billing"
|
|
209
|
+
|
|
210
|
+
namespace promises:
|
|
211
|
+
worker promisesWorker
|
|
212
|
+
options:
|
|
213
|
+
task_queue: "promises"
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# Signals, Queries, and Updates
|
|
2
|
+
|
|
3
|
+
> **Example:** [`signals-queries-updates.twf`](./signals-queries-updates.twf)
|
|
4
|
+
|
|
5
|
+
External communication with running workflows. These three primitives let code outside the workflow interact with it during execution — as a **read**, a **write**, or a **read-write**.
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
| Primitive | I/O | Direction | Execution | Use Case |
|
|
10
|
+
|-----------|-----|-----------|-----------|----------|
|
|
11
|
+
| **Query** | Read | External → Workflow → External | Sync (request-response) | Read current state |
|
|
12
|
+
| **Signal** | Write | External → Workflow | Async (fire-and-forget) | Events, notifications, data injection |
|
|
13
|
+
| **Update** | Read-write | External → Workflow → External | Sync (request-response) | Mutate state with confirmation |
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Signals
|
|
18
|
+
|
|
19
|
+
Asynchronous messages sent to a running workflow. The sender doesn't wait for processing.
|
|
20
|
+
|
|
21
|
+
### When to Use
|
|
22
|
+
|
|
23
|
+
- External events that workflow should react to
|
|
24
|
+
- Injecting data into a running workflow
|
|
25
|
+
- Triggering state transitions
|
|
26
|
+
- Human approval/rejection flows
|
|
27
|
+
|
|
28
|
+
### Signal Handler Bodies
|
|
29
|
+
|
|
30
|
+
Signals are declared with handler body blocks that execute when the signal arrives. Handler bodies have access to the full workflow statement set (activities, child workflows, timers, etc.).
|
|
31
|
+
|
|
32
|
+
```twf
|
|
33
|
+
signal PaymentReceived(transactionId: string, amount: decimal):
|
|
34
|
+
paymentStatus = "received"
|
|
35
|
+
lastTransactionId = transactionId
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The handler body executes when the signal arrives, whether via `await signal` or as a case in `await one`/`await all`. Handler bodies should primarily update workflow state. Heavy side effects (activity calls, child workflows) belong in the main workflow body after the signal is awaited, since handlers fire even when not being actively awaited and can execute between any two deterministic steps.
|
|
39
|
+
|
|
40
|
+
### Handler Execution Semantics
|
|
41
|
+
|
|
42
|
+
When a signal is awaited (via `await signal` or as an `await one` case), the execution order is:
|
|
43
|
+
|
|
44
|
+
1. **Signal arrives** → handler body runs first (updates state)
|
|
45
|
+
2. **Await resolves** → case body runs (reacts to updated state, calls activities, etc.)
|
|
46
|
+
|
|
47
|
+
This two-phase execution means:
|
|
48
|
+
|
|
49
|
+
```twf
|
|
50
|
+
workflow OrderTrackingWorkflow(orderId: string):
|
|
51
|
+
signal PaymentReceived(transactionId: string, amount: decimal):
|
|
52
|
+
# Phase 1: Handler runs immediately on signal arrival
|
|
53
|
+
paymentStatus = "received"
|
|
54
|
+
lastTransactionId = transactionId
|
|
55
|
+
|
|
56
|
+
# Phase 2: Case body runs after handler
|
|
57
|
+
await one:
|
|
58
|
+
signal PaymentReceived:
|
|
59
|
+
# paymentStatus is already "received" here
|
|
60
|
+
activity FulfillOrder(orderId, lastTransactionId)
|
|
61
|
+
close complete(OrderResult{status: "completed"})
|
|
62
|
+
timer(24h):
|
|
63
|
+
close fail(OrderResult{status: "timeout"})
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Key implications:**
|
|
67
|
+
- Handler bodies run on every signal arrival, even if the workflow isn't actively awaiting that signal
|
|
68
|
+
- Keep handler bodies lightweight (state updates only, no activities)
|
|
69
|
+
- Place activity calls and side effects in the `await one` case body, not the handler body
|
|
70
|
+
|
|
71
|
+
### Signal Considerations
|
|
72
|
+
|
|
73
|
+
| Consideration | Guidance |
|
|
74
|
+
|---------------|----------|
|
|
75
|
+
| **Ordering** | Signals are processed in order received, but arrival order isn't guaranteed |
|
|
76
|
+
| **Buffering** | Signals queue if workflow is busy; consider signal coalescing for high-volume |
|
|
77
|
+
| **Idempotency** | Signal handlers should be idempotent (same signal twice = same result) |
|
|
78
|
+
| **Validation** | Validate signal payload; invalid signals can corrupt workflow state |
|
|
79
|
+
| **Ambient arrival** | Signals can arrive between any two deterministic steps and are buffered until handled by `await signal` or `await one`/`await all` |
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Sending Signals to a Child Workflow
|
|
84
|
+
|
|
85
|
+
Everything above is the *receive* side — declaring handlers and awaiting arrivals. A workflow can also *send* a signal to a child it started and still holds a handle to. The handle is a workflow-bound promise (`promise handle <- workflow X(args)`); the dot-qualified name selects a signal the target workflow declares.
|
|
86
|
+
|
|
87
|
+
```twf
|
|
88
|
+
workflow OrderSaga(order: Order) -> (SagaResult):
|
|
89
|
+
promise pay <- workflow ProcessPayment(order)
|
|
90
|
+
promise ship <- workflow ShipOrder(order)
|
|
91
|
+
|
|
92
|
+
# Notify the payment workflow that the order has shipped
|
|
93
|
+
signal pay.OrderShipped(shipmentId)
|
|
94
|
+
|
|
95
|
+
# The handle is still awaitable later — sending does not consume it
|
|
96
|
+
await all:
|
|
97
|
+
await pay -> payment
|
|
98
|
+
await ship -> shipment
|
|
99
|
+
close complete(SagaResult{payment, shipment})
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The send is **statement-only and fire-and-forget**. There is no `await` or `promise` form and it is not an `await one` case: a signal carries no return value, so there is nothing to bind. The only thing a sender could wait on is *send acceptance* (the server accepting the send), never the receiver's handler running — modeling that would invite the misreading "the target processed my signal."
|
|
103
|
+
|
|
104
|
+
### Rules
|
|
105
|
+
|
|
106
|
+
- The handle must be **workflow-bound** (`promise h <- workflow X(args)`). Sending on a handle bound to a timer, signal, activity, etc. is an error.
|
|
107
|
+
- The target workflow must **declare** the named signal (`signal OrderShipped(...)` on `X`), or it is an error.
|
|
108
|
+
- A workflow-bound promise serves **two roles** on the same handle — an awaitable (`await pay -> payment`) and a signal target (`signal pay.OrderShipped(...)`). Sending a signal does not consume or affect a later `await` on it.
|
|
109
|
+
|
|
110
|
+
### When to Use
|
|
111
|
+
|
|
112
|
+
- Coordinating sibling/child workflows in a saga — telling one child about an event another produced.
|
|
113
|
+
- Pushing an event into a child you started, without waiting for it to react.
|
|
114
|
+
|
|
115
|
+
This is the only cross-workflow send the DSL provides: handle-bound only. Addressing a workflow you did not start (external/ID-based sends), and cross-workflow queries or updates, are not modeled.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Queries
|
|
120
|
+
|
|
121
|
+
Synchronous, read-only access to workflow state. The caller blocks until the query returns.
|
|
122
|
+
|
|
123
|
+
### When to Use
|
|
124
|
+
|
|
125
|
+
- UI needs to display current workflow state
|
|
126
|
+
- Monitoring/debugging workflow progress
|
|
127
|
+
- External system needs workflow data
|
|
128
|
+
- Building workflow dashboards
|
|
129
|
+
|
|
130
|
+
### Query Handler Bodies
|
|
131
|
+
|
|
132
|
+
Queries are declared with handler body blocks that execute when queried. Query handlers are restricted to activity-style statements (no temporal primitives like timers, signals, or child workflows).
|
|
133
|
+
|
|
134
|
+
```twf
|
|
135
|
+
query GetStatus() -> (string):
|
|
136
|
+
return status
|
|
137
|
+
|
|
138
|
+
query GetProgress() -> (Progress):
|
|
139
|
+
return Progress{status: status, processed: itemCount}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Query Considerations
|
|
143
|
+
|
|
144
|
+
| Consideration | Guidance |
|
|
145
|
+
|---------------|----------|
|
|
146
|
+
| **Read-only** | Queries MUST NOT modify workflow state |
|
|
147
|
+
| **Determinism** | Query handlers run during replay; must be deterministic |
|
|
148
|
+
| **Performance** | Queries replay workflow history; expensive for long histories |
|
|
149
|
+
| **Consistency** | Returns point-in-time state; may be stale by the time caller uses it |
|
|
150
|
+
| **Restrictions** | Query handlers use activity-restricted statement set (no timers, signals, workflows) |
|
|
151
|
+
|
|
152
|
+
### Anti-Patterns
|
|
153
|
+
|
|
154
|
+
```twf
|
|
155
|
+
# BAD: Query modifies state
|
|
156
|
+
query GetAndIncrementCounter() -> (int):
|
|
157
|
+
counter = counter + 1 # NOT ALLOWED
|
|
158
|
+
return counter
|
|
159
|
+
|
|
160
|
+
# GOOD: Pure read
|
|
161
|
+
query GetStatus() -> (string):
|
|
162
|
+
return status
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Updates
|
|
168
|
+
|
|
169
|
+
Synchronous read-write operations. The caller sends data, the workflow processes it, and the caller blocks until it receives a result (or error) back.
|
|
170
|
+
|
|
171
|
+
### When to Use
|
|
172
|
+
|
|
173
|
+
- Need confirmation that change was applied
|
|
174
|
+
- Validating input before accepting
|
|
175
|
+
- Returning computed result from mutation
|
|
176
|
+
- Request-response pattern with workflow
|
|
177
|
+
|
|
178
|
+
### Update Handler Bodies
|
|
179
|
+
|
|
180
|
+
Updates are declared with handler body blocks that execute when the update is received. Handler bodies have access to the full workflow statement set (activities, child workflows, timers, etc.) and **must return a value** to the caller.
|
|
181
|
+
|
|
182
|
+
Simple state mutation with immediate return:
|
|
183
|
+
|
|
184
|
+
```twf
|
|
185
|
+
update ChangePlan(newPlan: string) -> (ChangeResult):
|
|
186
|
+
plan = newPlan
|
|
187
|
+
return ChangeResult{success: true, plan: plan}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Validation via activity before accepting mutation:
|
|
191
|
+
|
|
192
|
+
```twf
|
|
193
|
+
update ChangePlan(newPlan: string) -> (ChangeResult):
|
|
194
|
+
activity ValidatePlan(newPlan) -> validation
|
|
195
|
+
if (validation.valid):
|
|
196
|
+
plan = newPlan
|
|
197
|
+
return ChangeResult{success: true, plan: plan}
|
|
198
|
+
else:
|
|
199
|
+
return ChangeResult{success: false, error: validation.reason}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
The caller blocks until the handler returns — including any time spent waiting on activities, child workflows, or timers within the handler.
|
|
203
|
+
|
|
204
|
+
### Handler Execution Semantics
|
|
205
|
+
|
|
206
|
+
Signal and update handlers run as coroutines alongside the main workflow body, but **only one piece of workflow code runs at a time** (cooperative scheduling). When a workflow wakes up, it processes pending messages (signals/updates) in order, then makes progress in the main workflow body.
|
|
207
|
+
|
|
208
|
+
This means:
|
|
209
|
+
1. The update handler runs as part of the workflow execution loop
|
|
210
|
+
2. If the handler blocks (on an activity, timer, etc.), the main workflow body can make progress while it waits
|
|
211
|
+
3. The handler reads from and writes to the same shared workflow state as the main body and signal handlers
|
|
212
|
+
4. The caller only receives a response after the handler has completed and returned
|
|
213
|
+
|
|
214
|
+
**Update handlers cannot call `close`** — they can mutate state and return values, but only the main workflow body can terminate the workflow.
|
|
215
|
+
|
|
216
|
+
### Awaiting Updates
|
|
217
|
+
|
|
218
|
+
Updates can be awaited in the workflow body, similar to signals. This is useful when the main workflow body needs to wait for an external mutation before continuing:
|
|
219
|
+
|
|
220
|
+
```twf
|
|
221
|
+
await update ChangeAddress
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Updates can also race against other operations in `await one`:
|
|
225
|
+
|
|
226
|
+
```twf
|
|
227
|
+
await one:
|
|
228
|
+
update ChangeAddress -> (newAddress):
|
|
229
|
+
activity NotifyShipping(orderId, newAddress)
|
|
230
|
+
timer(1h):
|
|
231
|
+
activity FinalizeShipping(orderId)
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
When the update wins the race, its handler body runs and returns a value to the caller, then the case body executes.
|
|
235
|
+
|
|
236
|
+
### Update Handlers with Conditions
|
|
237
|
+
|
|
238
|
+
A common pattern has an update handler wait on workflow state using `condition`. The caller blocks until the condition becomes true, then receives a result reflecting the current state:
|
|
239
|
+
|
|
240
|
+
```twf
|
|
241
|
+
workflow JobCoordinator(config: JobConfig):
|
|
242
|
+
state:
|
|
243
|
+
condition jobReady
|
|
244
|
+
|
|
245
|
+
signal Shutdown():
|
|
246
|
+
shutdownRequested = true
|
|
247
|
+
|
|
248
|
+
update WaitUntilReady() -> (JobState):
|
|
249
|
+
await jobReady
|
|
250
|
+
return JobState{ready: true}
|
|
251
|
+
|
|
252
|
+
# Main body provisions and starts the job runner
|
|
253
|
+
activity ProvisionJobRunner(config)
|
|
254
|
+
activity StartJobRunner(config)
|
|
255
|
+
set jobReady
|
|
256
|
+
|
|
257
|
+
await signal Shutdown
|
|
258
|
+
close complete
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
In this pattern:
|
|
262
|
+
1. The client calls the update and blocks waiting for a result
|
|
263
|
+
2. The update handler starts running but yields on `await jobReady`
|
|
264
|
+
3. The main workflow body mutates the condition via `set jobReady`
|
|
265
|
+
4. The update handler resumes and returns a value
|
|
266
|
+
5. The client receives the result
|
|
267
|
+
|
|
268
|
+
See [promises-conditions.md](./promises-conditions.md) for more on conditions and the `state:` block.
|
|
269
|
+
|
|
270
|
+
### Updates vs Signals
|
|
271
|
+
|
|
272
|
+
| Aspect | Signal (write) | Update (read-write) |
|
|
273
|
+
|--------|--------|--------|
|
|
274
|
+
| **Response** | None (fire-and-forget) | Returns result to caller |
|
|
275
|
+
| **Validation** | In handler, but caller doesn't know | Caller receives validation errors |
|
|
276
|
+
| **Confirmation** | No guarantee processing happened | Caller knows when complete |
|
|
277
|
+
| **Handler can block** | Yes, but caller doesn't wait | Yes, and caller blocks until done |
|
|
278
|
+
| **Use when** | "Notify workflow of X" | "Change X and tell me if it worked" |
|
|
279
|
+
|
|
280
|
+
### Update Considerations
|
|
281
|
+
|
|
282
|
+
| Consideration | Guidance |
|
|
283
|
+
|---------------|----------|
|
|
284
|
+
| **Atomicity** | Update handlers should be atomic; don't leave partial state |
|
|
285
|
+
| **Validation** | Validate before mutating; return errors, don't throw |
|
|
286
|
+
| **Idempotency** | Consider idempotency keys for critical updates |
|
|
287
|
+
| **Timeouts** | Caller should set appropriate timeout; handler may block on activities or state |
|
|
288
|
+
| **Shared state** | Handler reads/writes the same state as the main workflow body and signal handlers |
|
|
289
|
+
| **Ambient arrival** | Like signals, updates can arrive between any two deterministic steps and are buffered until handled by `await update` or `await one`/`await all` |
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Choosing Between Primitives
|
|
294
|
+
|
|
295
|
+
**Use QUERY (read) when:**
|
|
296
|
+
- Need to read current state
|
|
297
|
+
- Building UI/dashboard
|
|
298
|
+
- Debugging/monitoring
|
|
299
|
+
|
|
300
|
+
**Use SIGNAL (write) when:**
|
|
301
|
+
- Fire-and-forget is acceptable
|
|
302
|
+
- External event notification
|
|
303
|
+
- No response needed
|
|
304
|
+
|
|
305
|
+
**Use UPDATE (read-write) when:**
|
|
306
|
+
- Need confirmation that a change was applied
|
|
307
|
+
- Validating input before accepting
|
|
308
|
+
- Returning a computed result from a mutation
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## Signal/Query/Update Naming Conventions
|
|
313
|
+
|
|
314
|
+
| Type | Convention | Examples |
|
|
315
|
+
|------|------------|----------|
|
|
316
|
+
| Signals | Event-style, past tense or imperative | `PaymentReceived`, `Cancel`, `AddItem` |
|
|
317
|
+
| Queries | Getter-style, "Get" prefix | `GetStatus`, `GetProgress`, `GetItems` |
|
|
318
|
+
| Updates | Action-style, verb phrase | `ChangePlan`, `AddCredits`, `UpdateAddress` |
|
|
319
|
+
|