@tenantegroup/ai-rules-mcp 1.0.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/INSTALLATION.md +52 -0
- package/README.md +57 -0
- package/USAGE.md +46 -0
- package/package.json +57 -0
- package/rules/cloudflare/api-services.md +80 -0
- package/rules/cloudflare/cicd-deployment.md +56 -0
- package/rules/cloudflare/database-orm.md +28 -0
- package/rules/cloudflare/edge-parity.md +24 -0
- package/rules/cloudflare/kv-usage.md +31 -0
- package/rules/cloudflare/logging-observability.md +66 -0
- package/rules/cloudflare/performance.md +44 -0
- package/rules/cloudflare/realtime-background.md +58 -0
- package/rules/cloudflare/security.md +162 -0
- package/rules/cloudflare/seeding.md +27 -0
- package/rules/cloudflare/workflows.md +593 -0
- package/rules/dotnet/api.md +26 -0
- package/rules/dotnet/architecture.md +27 -0
- package/rules/dotnet/cli.md +26 -0
- package/rules/dotnet/configuration.md +26 -0
- package/rules/dotnet/logging.md +25 -0
- package/rules/dotnet/maui.md +26 -0
- package/rules/dotnet/mvvm.md +26 -0
- package/rules/dotnet/packaging.md +24 -0
- package/rules/dotnet/project-structure.md +26 -0
- package/rules/dotnet/sqlite.md +29 -0
- package/rules/dotnet/testing.md +24 -0
- package/rules/flutter/api.md +29 -0
- package/rules/flutter/architecture.md +34 -0
- package/rules/flutter/auth.md +27 -0
- package/rules/flutter/configuration.md +24 -0
- package/rules/flutter/database.md +30 -0
- package/rules/flutter/logging.md +27 -0
- package/rules/flutter/navigation.md +28 -0
- package/rules/flutter/offline-sync.md +26 -0
- package/rules/flutter/platform.md +30 -0
- package/rules/flutter/project-structure.md +32 -0
- package/rules/flutter/riverpod.md +32 -0
- package/rules/flutter/testing.md +31 -0
- package/rules/nuxt/architecture-principles.md +31 -0
- package/rules/nuxt/authentication.md +35 -0
- package/rules/nuxt/code-quality.md +71 -0
- package/rules/nuxt/configuration.md +31 -0
- package/rules/nuxt/core-directives.md +12 -0
- package/rules/nuxt/project-initialization.md +53 -0
- package/rules/nuxt/project-structure.md +44 -0
- package/rules/nuxt/testing.md +48 -0
- package/src/index.js +757 -0
- package/templates/cloudflare/compile-context.js +43 -0
- package/templates/cloudflare/hooks/post-checkout +5 -0
- package/templates/cloudflare/hooks/pre-commit +14 -0
- package/templates/cloudflare/install-hooks.js +34 -0
- package/templates/cloudflare/validate-code.js +57 -0
- package/templates/dotnet/compile-context.js +43 -0
- package/templates/dotnet/hooks/post-checkout +5 -0
- package/templates/dotnet/hooks/pre-commit +14 -0
- package/templates/dotnet/install-hooks.js +34 -0
- package/templates/dotnet/validate-code.js +84 -0
- package/templates/flutter/compile-context.js +43 -0
- package/templates/flutter/hooks/post-checkout +5 -0
- package/templates/flutter/hooks/pre-commit +14 -0
- package/templates/flutter/install-hooks.js +34 -0
- package/templates/flutter/validate-code.js +64 -0
- package/templates/nuxt/compile-context.js +43 -0
- package/templates/nuxt/hooks/post-checkout +5 -0
- package/templates/nuxt/hooks/pre-commit +14 -0
- package/templates/nuxt/install-hooks.js +34 -0
- package/templates/nuxt/validate-code.js +57 -0
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
# Cloudflare Workflows
|
|
2
|
+
|
|
3
|
+
## Core Principle
|
|
4
|
+
Workflows enable durable, multi-step orchestration with automatic retry, state persistence, and saga-pattern rollback support. Use Workflows for any operation that requires:
|
|
5
|
+
- Coordinating multiple steps across external systems
|
|
6
|
+
- Long-running operations (minutes to weeks)
|
|
7
|
+
- Automatic recovery from failures with state persistence
|
|
8
|
+
- Semantic reversal of operations on failure (saga pattern)
|
|
9
|
+
- Human-in-the-loop approvals or event-driven pauses
|
|
10
|
+
|
|
11
|
+
## When to Use Workflows
|
|
12
|
+
|
|
13
|
+
### Use Workflows For
|
|
14
|
+
- **Multi-step transactions** — Fund transfers, payment processing, order fulfillment that span external systems (banks, payment processors, inventory systems)
|
|
15
|
+
- **Long-running orchestration** — Data pipelines, image processing, AI-powered workflows lasting hours or days
|
|
16
|
+
- **Event-driven workflows** — Wait for approvals, user input, webhook responses, or external system callbacks before proceeding
|
|
17
|
+
- **State-machine logic** — Complex workflows with multiple paths, retries, and compensating actions
|
|
18
|
+
- **User lifecycle automation** — Onboarding, trial expirations, automated emails with pauses and external event dependencies
|
|
19
|
+
- **Human-in-the-loop systems** — Pause workflows for approval, resume programmatically after decision
|
|
20
|
+
|
|
21
|
+
### Do NOT Use Workflows For
|
|
22
|
+
- Simple fire-and-forget async tasks (use **Queues** instead)
|
|
23
|
+
- Stateless, millisecond-scale operations (use **Workers** instead)
|
|
24
|
+
- Real-time collaboration requiring coordinated client state (use **Durable Objects** instead)
|
|
25
|
+
- Pure caching or session storage (use **KV** instead)
|
|
26
|
+
|
|
27
|
+
## Workflow Architecture
|
|
28
|
+
|
|
29
|
+
### Durable Steps (`step.do()`)
|
|
30
|
+
|
|
31
|
+
Every step in a Workflow is durable — execution is logged, state is persisted, and retries are automatic:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
export class ProcessPaymentWorkflow extends WorkflowEntrypoint {
|
|
35
|
+
async run(event: WorkflowEvent, step: WorkflowStep) {
|
|
36
|
+
// Step 1: Charge the card
|
|
37
|
+
const charge = await step.do("charge-card", async () => {
|
|
38
|
+
return await this.env.PAYMENT.charge({
|
|
39
|
+
amount: event.payload.amount,
|
|
40
|
+
cardToken: event.payload.token,
|
|
41
|
+
idempotencyKey: `${event.payload.orderId}:charge`,
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Step 2: Update inventory (fails if charge succeeds but this fails)
|
|
46
|
+
const inventory = await step.do("deduct-inventory", async () => {
|
|
47
|
+
return await this.env.DB.prepare(
|
|
48
|
+
"UPDATE products SET stock = stock - 1 WHERE id = ?"
|
|
49
|
+
).bind(event.payload.productId).first();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Step 3: Send confirmation
|
|
53
|
+
await step.do("send-email", async () => {
|
|
54
|
+
await this.env.MAILER.send({
|
|
55
|
+
to: event.payload.email,
|
|
56
|
+
subject: "Order Confirmation",
|
|
57
|
+
body: "Your order has been confirmed",
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Key Properties:**
|
|
65
|
+
- Each `step.do()` is uniquely named (name is the identifier)
|
|
66
|
+
- Step body executes exactly once; output is memoized
|
|
67
|
+
- Retries are automatic on transient failures
|
|
68
|
+
- If a step fails after maximum retries, the Workflow fails
|
|
69
|
+
|
|
70
|
+
### Saga Pattern Rollbacks
|
|
71
|
+
|
|
72
|
+
The **saga pattern** defines a compensating action for each step. If a Workflow fails, rollback handlers execute in **reverse order** (LIFO), undoing completed steps.
|
|
73
|
+
|
|
74
|
+
#### Without Rollbacks (Manual Compensation)
|
|
75
|
+
```typescript
|
|
76
|
+
let chargeId;
|
|
77
|
+
let inventoryDeducted = false;
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
chargeId = await step.do("charge-card", async () => {
|
|
81
|
+
return await this.env.PAYMENT.charge(...);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
inventoryDeducted = await step.do("deduct-inventory", async () => {
|
|
85
|
+
return await this.env.DB.prepare(...).first();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// If email fails, manual cleanup outside steps
|
|
89
|
+
await step.do("send-email", async () => {
|
|
90
|
+
// Fails here — how do we undo inventory?
|
|
91
|
+
throw new Error("Email service down");
|
|
92
|
+
});
|
|
93
|
+
} catch (error) {
|
|
94
|
+
// Manual unwinding — brittle, error-prone, easy to forget steps
|
|
95
|
+
if (inventoryDeducted) {
|
|
96
|
+
await step.do("undo-inventory", async () => {
|
|
97
|
+
return await this.env.DB.prepare(
|
|
98
|
+
"UPDATE products SET stock = stock + 1 WHERE id = ?"
|
|
99
|
+
).bind(event.payload.productId).first();
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (chargeId) {
|
|
103
|
+
await step.do("refund-charge", async () => {
|
|
104
|
+
return await this.env.PAYMENT.refund({
|
|
105
|
+
chargeId,
|
|
106
|
+
idempotencyKey: `${event.payload.orderId}:refund`,
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
#### With Rollbacks (Built-In Compensation)
|
|
115
|
+
```typescript
|
|
116
|
+
export class ProcessPaymentWorkflow extends WorkflowEntrypoint {
|
|
117
|
+
async run(event: WorkflowEvent, step: WorkflowStep) {
|
|
118
|
+
// Charge card — if needed, refund
|
|
119
|
+
const charge = await step.do(
|
|
120
|
+
"charge-card",
|
|
121
|
+
async () => {
|
|
122
|
+
return await this.env.PAYMENT.charge({
|
|
123
|
+
amount: event.payload.amount,
|
|
124
|
+
cardToken: event.payload.token,
|
|
125
|
+
idempotencyKey: `${event.payload.orderId}:charge`,
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
rollback: async ({ output }) => {
|
|
130
|
+
// output is the charge object returned from step.do()
|
|
131
|
+
if (output && output.id) {
|
|
132
|
+
await this.env.PAYMENT.refund({
|
|
133
|
+
chargeId: output.id,
|
|
134
|
+
idempotencyKey: `${event.payload.orderId}:rollback-charge`,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// Deduct inventory — if needed, restock
|
|
142
|
+
const inventory = await step.do(
|
|
143
|
+
"deduct-inventory",
|
|
144
|
+
async () => {
|
|
145
|
+
return await this.env.DB.prepare(
|
|
146
|
+
"UPDATE products SET stock = stock - 1 WHERE id = ? RETURNING *"
|
|
147
|
+
).bind(event.payload.productId).first();
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
rollback: async ({ output }) => {
|
|
151
|
+
// Restock inventory (must be idempotent)
|
|
152
|
+
await this.env.DB.prepare(
|
|
153
|
+
"UPDATE products SET stock = stock + 1 WHERE id = ?"
|
|
154
|
+
).bind(event.payload.productId).run();
|
|
155
|
+
},
|
|
156
|
+
}
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
// Send confirmation (no rollback needed — email is idempotent)
|
|
160
|
+
await step.do("send-email", async () => {
|
|
161
|
+
await this.env.MAILER.send({
|
|
162
|
+
to: event.payload.email,
|
|
163
|
+
subject: "Order Confirmation",
|
|
164
|
+
body: "Your order has been confirmed",
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**Rollback Rules:**
|
|
172
|
+
1. **Rollback handlers execute in reverse LIFO order** — If steps A → B → C run, and C fails, rollbacks run C → B → A
|
|
173
|
+
2. **Only failed Workflows trigger rollback** — If user code catches an error and Workflow continues, no rollback. Rollback only starts when Workflow is about to fail terminally
|
|
174
|
+
3. **Failed steps CAN still rollback** — A step that fails may have partially succeeded (e.g., charge captured but transient error before returning). Rollback handlers receive `output`, but must handle `output === undefined`
|
|
175
|
+
4. **Rollback handlers MUST be idempotent** — Design as if they will be called multiple times. Use idempotency keys for all external operations:
|
|
176
|
+
```typescript
|
|
177
|
+
// Refund with idempotency key — safe to retry
|
|
178
|
+
await this.env.PAYMENT.refund({
|
|
179
|
+
chargeId: output.id,
|
|
180
|
+
idempotencyKey: `${orderId}:rollback-charge`, // ← Same key on retry
|
|
181
|
+
});
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## State Persistence & Long-Running Workflows
|
|
185
|
+
|
|
186
|
+
Workflows persist state across the entire execution, even if the Worker isolate is recycled:
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
export class DataProcessingWorkflow extends WorkflowEntrypoint {
|
|
190
|
+
async run(event: WorkflowEvent, step: WorkflowStep) {
|
|
191
|
+
// Pause workflow for 7 days (state is persisted)
|
|
192
|
+
await step.sleep("wait-7-days", "7 days");
|
|
193
|
+
|
|
194
|
+
// Process data
|
|
195
|
+
const result = await step.do("process-data", async () => {
|
|
196
|
+
return await expensiveProcessing(event.payload);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Pause for user approval (wait for external event)
|
|
200
|
+
const approval = await step.waitForEvent("await-approval", {
|
|
201
|
+
event: "user-approved",
|
|
202
|
+
timeout: "24 hours",
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
if (approval.data.approved) {
|
|
206
|
+
await step.do("publish", async () => {
|
|
207
|
+
await this.env.STORAGE.put(`published/${event.payload.id}`, result);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**Key Features:**
|
|
215
|
+
- **`step.sleep(name, duration)`** — Pause for seconds, hours, days, weeks
|
|
216
|
+
- **`step.sleepUntil(name, timestamp)`** — Resume at specific ISO timestamp
|
|
217
|
+
- **`step.waitForEvent(name, options)`** — Pause until external event (webhook, API call)
|
|
218
|
+
- **State is persisted across pauses** — Isolate recycling, server shutdown, etc. do not affect execution
|
|
219
|
+
- **Timeouts prevent forever-waiting** — `waitForEvent()` supports `timeout` to terminate if event never arrives
|
|
220
|
+
|
|
221
|
+
## Workflow Lifecycle Management
|
|
222
|
+
|
|
223
|
+
### Triggering Workflows
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
// From a Worker/API endpoint, trigger a new Workflow instance
|
|
227
|
+
export default {
|
|
228
|
+
async fetch(request, env) {
|
|
229
|
+
const workflowId = crypto.randomUUID();
|
|
230
|
+
|
|
231
|
+
await env.WORKFLOWS.create(ProcessPaymentWorkflow, {
|
|
232
|
+
id: workflowId,
|
|
233
|
+
params: {
|
|
234
|
+
orderId: "order-123",
|
|
235
|
+
amount: 9999,
|
|
236
|
+
email: "user@example.com",
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
return new Response(
|
|
241
|
+
JSON.stringify({ workflowId, status: "created" }),
|
|
242
|
+
{ status: 202 }
|
|
243
|
+
);
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Pausing & Resuming Workflows
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
// Pause a running Workflow
|
|
252
|
+
await env.WORKFLOWS.pause(workflowId);
|
|
253
|
+
|
|
254
|
+
// Resume a paused Workflow
|
|
255
|
+
await env.WORKFLOWS.resume(workflowId);
|
|
256
|
+
|
|
257
|
+
// Terminate a Workflow
|
|
258
|
+
await env.WORKFLOWS.terminate(workflowId);
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Querying Workflow Status
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
// Get current Workflow status (used for polling or dashboard)
|
|
265
|
+
const status = await env.WORKFLOWS.getStatus(workflowId);
|
|
266
|
+
// Returns: { id, status, steps, history, nextScheduledTime, output, error }
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Configuration & Binding
|
|
270
|
+
|
|
271
|
+
### wrangler.toml
|
|
272
|
+
|
|
273
|
+
Workflows do NOT require explicit binding; they are built-in to the Workers runtime:
|
|
274
|
+
|
|
275
|
+
```toml
|
|
276
|
+
# wrangler.toml
|
|
277
|
+
name = "my-app"
|
|
278
|
+
main = "src/index.ts"
|
|
279
|
+
|
|
280
|
+
[[workflows]]
|
|
281
|
+
binding = "WORKFLOWS"
|
|
282
|
+
name = "my_workflows"
|
|
283
|
+
|
|
284
|
+
# Bind the workflow class
|
|
285
|
+
[[env.production.workflows]]
|
|
286
|
+
binding = "WORKFLOWS"
|
|
287
|
+
class = "ProcessPaymentWorkflow"
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Database Access from Workflows
|
|
291
|
+
|
|
292
|
+
Workflows can access D1, KV, and other Cloudflare bindings the same way Workers do:
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
export class UserOnboardingWorkflow extends WorkflowEntrypoint {
|
|
296
|
+
async run(event: WorkflowEvent, step: WorkflowStep) {
|
|
297
|
+
const user = await step.do("fetch-user", async () => {
|
|
298
|
+
return await this.env.DB.prepare(
|
|
299
|
+
"SELECT * FROM users WHERE id = ?"
|
|
300
|
+
).bind(event.payload.userId).first();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// Send welcome email
|
|
304
|
+
await step.do("send-welcome-email", async () => {
|
|
305
|
+
await this.env.MAILER.send({
|
|
306
|
+
to: user.email,
|
|
307
|
+
subject: "Welcome!",
|
|
308
|
+
body: `Hi ${user.name}`,
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// Set trial expiration (14 days from now)
|
|
313
|
+
await step.sleep("wait-trial", "14 days");
|
|
314
|
+
|
|
315
|
+
// Check if user has converted
|
|
316
|
+
const hasConverted = await step.do("check-conversion", async () => {
|
|
317
|
+
const subscription = await this.env.DB.prepare(
|
|
318
|
+
"SELECT * FROM subscriptions WHERE user_id = ? AND status = 'active'"
|
|
319
|
+
).bind(event.payload.userId).first();
|
|
320
|
+
return !!subscription;
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
if (!hasConverted) {
|
|
324
|
+
// Send expiration notice
|
|
325
|
+
await step.do("send-expiration-email", async () => {
|
|
326
|
+
await this.env.MAILER.send({
|
|
327
|
+
to: user.email,
|
|
328
|
+
subject: "Your trial has ended",
|
|
329
|
+
body: "Subscribe now to continue",
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
## Error Handling & Observability
|
|
338
|
+
|
|
339
|
+
### Retry Behavior
|
|
340
|
+
|
|
341
|
+
Workflows automatically retry failed steps with exponential backoff:
|
|
342
|
+
- Default max retries: 5 (configurable)
|
|
343
|
+
- Backoff: exponential with jitter
|
|
344
|
+
- If a step exceeds max retries, the Workflow fails and rollback begins
|
|
345
|
+
|
|
346
|
+
### Logging from Workflows
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
export class OrderWorkflow extends WorkflowEntrypoint {
|
|
350
|
+
async run(event: WorkflowEvent, step: WorkflowStep) {
|
|
351
|
+
// Use console for structured logging (same as Workers)
|
|
352
|
+
console.log(JSON.stringify({
|
|
353
|
+
event: "workflow_start",
|
|
354
|
+
workflowId: event.id,
|
|
355
|
+
orderId: event.payload.orderId,
|
|
356
|
+
timestamp: new Date().toISOString(),
|
|
357
|
+
}));
|
|
358
|
+
|
|
359
|
+
const charge = await step.do("charge", async () => {
|
|
360
|
+
const result = await this.env.PAYMENT.charge({...});
|
|
361
|
+
console.log(JSON.stringify({
|
|
362
|
+
event: "charge_success",
|
|
363
|
+
chargeId: result.id,
|
|
364
|
+
amount: result.amount,
|
|
365
|
+
}));
|
|
366
|
+
return result;
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Debugging in Dashboard
|
|
373
|
+
|
|
374
|
+
- **Cloudflare Dashboard** → **Workflows** → View step-by-step execution history
|
|
375
|
+
- See input/output for each step
|
|
376
|
+
- View retry attempts and timing
|
|
377
|
+
- Inspect rollback execution order
|
|
378
|
+
|
|
379
|
+
## Idempotency & Distributed Systems
|
|
380
|
+
|
|
381
|
+
### Critical: External Idempotency Keys
|
|
382
|
+
|
|
383
|
+
Since steps can be retried, **all external operations MUST use idempotency keys** to prevent duplicate side effects:
|
|
384
|
+
|
|
385
|
+
```typescript
|
|
386
|
+
// ❌ NOT IDEMPOTENT — repeated calls charge multiple times
|
|
387
|
+
await this.env.PAYMENT.charge({
|
|
388
|
+
amount: 9999,
|
|
389
|
+
card: token,
|
|
390
|
+
// No idempotency key — if Workflow retries, user charged twice
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// ✅ IDEMPOTENT — repeated calls use same key, provider deduplicates
|
|
394
|
+
await this.env.PAYMENT.charge({
|
|
395
|
+
amount: 9999,
|
|
396
|
+
card: token,
|
|
397
|
+
idempotencyKey: `${orderId}:charge:attempt1`,
|
|
398
|
+
});
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### D1 Transactions
|
|
402
|
+
|
|
403
|
+
For D1 operations, use Drizzle transactions to ensure atomic steps:
|
|
404
|
+
|
|
405
|
+
```typescript
|
|
406
|
+
const result = await step.do("create-order", async () => {
|
|
407
|
+
return await this.env.DB.transaction(async (tx) => {
|
|
408
|
+
const order = await tx.insert(ordersTable).values({
|
|
409
|
+
id: event.payload.orderId,
|
|
410
|
+
amount: event.payload.amount,
|
|
411
|
+
status: "pending",
|
|
412
|
+
}).returning();
|
|
413
|
+
|
|
414
|
+
await tx.insert(orderItemsTable).values(
|
|
415
|
+
event.payload.items.map((item) => ({
|
|
416
|
+
orderId: order[0].id,
|
|
417
|
+
productId: item.productId,
|
|
418
|
+
quantity: item.quantity,
|
|
419
|
+
}))
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
return order[0];
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
## Deployment & Versioning
|
|
428
|
+
|
|
429
|
+
### Deploy Workflows with Wrangler
|
|
430
|
+
|
|
431
|
+
```bash
|
|
432
|
+
# Deploy to production
|
|
433
|
+
wrangler deploy
|
|
434
|
+
|
|
435
|
+
# Deploy to staging
|
|
436
|
+
wrangler deploy --env staging
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
Workflows are versioned with your application code. If you deploy a new Workflow definition:
|
|
440
|
+
- New instances use the new definition
|
|
441
|
+
- Running instances continue with the old definition (no breaking mid-flight)
|
|
442
|
+
- After deploying, new workflow IDs reference the new version
|
|
443
|
+
|
|
444
|
+
### Updating Running Workflows
|
|
445
|
+
|
|
446
|
+
Avoid changing Workflow definitions while instances are running:
|
|
447
|
+
|
|
448
|
+
```typescript
|
|
449
|
+
// ❌ RISKY — Modifying a running Workflow can cause inconsistency
|
|
450
|
+
export class OrderWorkflow extends WorkflowEntrypoint {
|
|
451
|
+
async run(event: WorkflowEvent, step: WorkflowStep) {
|
|
452
|
+
// Previously: step 1 was "charge"
|
|
453
|
+
// Now: renamed to "charge-card"
|
|
454
|
+
// Running instances fail because state history expects "charge"
|
|
455
|
+
await step.do("charge-card", ...); // ← Causes desynchronization
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ✅ SAFE — Create new version, let old instances finish
|
|
460
|
+
export class OrderWorkflowV2 extends WorkflowEntrypoint {
|
|
461
|
+
async run(event: WorkflowEvent, step: WorkflowStep) {
|
|
462
|
+
// New Workflow class, old instances unaffected
|
|
463
|
+
await step.do("charge-card", ...);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
Best practice: Trigger new instances to use `OrderWorkflowV2`, and let old `OrderWorkflow` instances complete.
|
|
469
|
+
|
|
470
|
+
## Best Practices
|
|
471
|
+
|
|
472
|
+
### 1. Keep Steps Focused
|
|
473
|
+
- One step = one logical operation
|
|
474
|
+
- Avoid grouping multiple external calls in a single step
|
|
475
|
+
```typescript
|
|
476
|
+
// ❌ TOO BROAD
|
|
477
|
+
await step.do("process-order", async () => {
|
|
478
|
+
const charge = await this.env.PAYMENT.charge(...);
|
|
479
|
+
const inventory = await this.env.DB.updateInventory(...);
|
|
480
|
+
const email = await this.env.MAILER.send(...);
|
|
481
|
+
return { charge, inventory, email };
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// ✅ FOCUSED
|
|
485
|
+
await step.do("charge", async () => {
|
|
486
|
+
return await this.env.PAYMENT.charge(...);
|
|
487
|
+
});
|
|
488
|
+
await step.do("deduct-inventory", async () => {
|
|
489
|
+
return await this.env.DB.updateInventory(...);
|
|
490
|
+
});
|
|
491
|
+
await step.do("send-email", async () => {
|
|
492
|
+
return await this.env.MAILER.send(...);
|
|
493
|
+
});
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
### 2. Always Provide Rollback for External State Changes
|
|
497
|
+
```typescript
|
|
498
|
+
// ✅ Fund transfer with rollback
|
|
499
|
+
await step.do(
|
|
500
|
+
"debit-source",
|
|
501
|
+
() => bankA.debit(from, amount),
|
|
502
|
+
{
|
|
503
|
+
rollback: async ({ output }) => {
|
|
504
|
+
bankA.credit(from, amount, output.id);
|
|
505
|
+
},
|
|
506
|
+
}
|
|
507
|
+
);
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
### 3. Use Timeouts on External Waits
|
|
511
|
+
```typescript
|
|
512
|
+
// ✅ Wait for approval with timeout
|
|
513
|
+
const approval = await step.waitForEvent("approval", {
|
|
514
|
+
event: "user-approved",
|
|
515
|
+
timeout: "24 hours", // Fail if no approval within 24 hours
|
|
516
|
+
});
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
### 4. Implement Application-Level Idempotency Checks
|
|
520
|
+
```typescript
|
|
521
|
+
// ✅ Check if order already exists before creating
|
|
522
|
+
const existing = await step.do("check-existing", async () => {
|
|
523
|
+
return await this.env.DB.prepare(
|
|
524
|
+
"SELECT id FROM orders WHERE id = ?"
|
|
525
|
+
).bind(event.payload.orderId).first();
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
if (existing) {
|
|
529
|
+
console.log("Order already processed");
|
|
530
|
+
return { status: "already_completed", orderId: existing.id };
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Proceed with creation
|
|
534
|
+
await step.do("create-order", async () => {
|
|
535
|
+
return await this.env.DB.prepare(
|
|
536
|
+
"INSERT INTO orders (id, ...) VALUES (?, ...)"
|
|
537
|
+
).bind(event.payload.orderId, ...).run();
|
|
538
|
+
});
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
### 5. Monitor Workflow Metrics
|
|
542
|
+
- Track execution time per step
|
|
543
|
+
- Monitor rollback frequency
|
|
544
|
+
- Alert on max retries exceeded
|
|
545
|
+
- Log all external API calls with timing and status
|
|
546
|
+
|
|
547
|
+
### 6. Design for Long Pauses
|
|
548
|
+
```typescript
|
|
549
|
+
// ✅ Designed for week-long pause
|
|
550
|
+
export class TrialExpirationWorkflow extends WorkflowEntrypoint {
|
|
551
|
+
async run(event: WorkflowEvent, step: WorkflowStep) {
|
|
552
|
+
// Pause for entire trial period
|
|
553
|
+
await step.sleep("trial-period", "14 days");
|
|
554
|
+
|
|
555
|
+
// Check conversion status
|
|
556
|
+
const converted = await step.do("check-conversion", async () => {
|
|
557
|
+
const sub = await this.env.DB.prepare(
|
|
558
|
+
"SELECT * FROM subscriptions WHERE user_id = ?"
|
|
559
|
+
).bind(event.payload.userId).first();
|
|
560
|
+
return !!sub?.activeUntil;
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
// Send appropriate email based on conversion
|
|
564
|
+
await step.do("notify", async () => {
|
|
565
|
+
if (converted) {
|
|
566
|
+
// Already subscribed — send nothing
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
// Trial ended — send upgrade email
|
|
570
|
+
await this.env.MAILER.send({...});
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
## When to Choose Between Workflows, Queues, and Durable Objects
|
|
577
|
+
|
|
578
|
+
| Feature | Workflows | Queues | Durable Objects |
|
|
579
|
+
|---------|-----------|--------|-----------------|
|
|
580
|
+
| **Multi-step orchestration** | ✅ Native | ⚠️ Manual | ❌ Not designed for |
|
|
581
|
+
| **Long-running (hours/days)** | ✅ Yes | ❌ ~15 min limit | ✅ Yes, but stateless |
|
|
582
|
+
| **State persistence** | ✅ Yes | ❌ No | ✅ Yes, but in-memory |
|
|
583
|
+
| **Automatic retries** | ✅ Yes | ✅ Yes | ❌ No |
|
|
584
|
+
| **Saga pattern rollback** | ✅ Yes (native) | ❌ Manual | ❌ Not applicable |
|
|
585
|
+
| **Wait for external events** | ✅ Yes | ❌ No | ❌ No |
|
|
586
|
+
| **Cost per operation** | Low (pay per step) | Low (pay per message) | High (compute-intensive) |
|
|
587
|
+
| **Use case** | Orchestration, transactions, approvals | Simple async tasks, email | Realtime coordination, chat |
|
|
588
|
+
|
|
589
|
+
**Decision Tree:**
|
|
590
|
+
- Need to orchestrate multiple external systems? → **Workflows**
|
|
591
|
+
- Need simple fire-and-forget async tasks? → **Queues**
|
|
592
|
+
- Need coordinated state across multiple clients? → **Durable Objects**
|
|
593
|
+
- Otherwise → Use **Workers** directly
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Rule: Enforce API Integration with Refit
|
|
2
|
+
Requirements:
|
|
3
|
+
- AI MUST follow all `.ai/rules/*.md` files and prioritize architecture constraints.
|
|
4
|
+
- AI MUST use Refit interfaces for HTTP API contracts.
|
|
5
|
+
- AI MUST use strongly typed DTOs for requests and responses.
|
|
6
|
+
- AI MUST wrap Refit interfaces in service classes before ViewModel consumption.
|
|
7
|
+
- AI MUST configure authenticated requests via DelegatingHandler.
|
|
8
|
+
- AI MUST set request timeout to 30 seconds unless feature-specific override is required.
|
|
9
|
+
- AI MUST implement retry policy: 3 retries with exponential backoff (250ms, 500ms, 1000ms) for transient network failures and HTTP 5xx only.
|
|
10
|
+
- AI MUST map API DTOs into domain/data models before persistence or UI use.
|
|
11
|
+
- AI MUST persist API results to SQLite before UI reads where offline-first applies.
|
|
12
|
+
- AI MUST handle `ApiException` and surface safe errors.
|
|
13
|
+
Prohibited:
|
|
14
|
+
- AI MUST NOT call Refit interfaces directly from Views or ViewModels.
|
|
15
|
+
- AI MUST NOT use raw `HttpClient` for standard API integration.
|
|
16
|
+
- AI MUST NOT log auth headers, tokens, passwords, or secret payload fields.
|
|
17
|
+
- AI MUST NOT use dynamic/weakly typed API payload handling.
|
|
18
|
+
- AI MUST NOT bypass retry/error handling constraints.
|
|
19
|
+
Patterns:
|
|
20
|
+
- `public interface IUserApi { [Get("/users/me")] Task<UserDto> GetCurrentUserAsync(); }`
|
|
21
|
+
- `UserApiService` depends on `IUserApi`.
|
|
22
|
+
- Authentication handler injects bearer token from secure secret provider.
|
|
23
|
+
- Service flow: Refit call -> DTO mapping -> repository save -> ViewModel read.
|
|
24
|
+
Examples:
|
|
25
|
+
- `builder.Services.AddRefitClient<IUserApi>().ConfigureHttpClient(c => c.BaseAddress = new Uri(baseUrl));`
|
|
26
|
+
- `catch (ApiException ex) { ... }`
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Rule: Enforce Application Architecture
|
|
2
|
+
Requirements:
|
|
3
|
+
- AI MUST follow this file and all `.ai/rules/*.md` files before generating any code.
|
|
4
|
+
- AI MUST prioritize these rules over default generation behavior.
|
|
5
|
+
- AI MUST use .NET MAUI with MVVM via CommunityToolkit.Mvvm.
|
|
6
|
+
- AI MUST keep strict separation: Views = UI only, ViewModels = state/commands only, Services = business logic.
|
|
7
|
+
- AI MUST use XAML-first UI and MAUI Shell URI routing.
|
|
8
|
+
- AI MUST use dependency injection for all dependencies.
|
|
9
|
+
- AI MUST register dependencies only in composition root files (`MauiProgram.cs` or `Program.cs`).
|
|
10
|
+
- AI MUST register services through interfaces.
|
|
11
|
+
- AI MUST register ViewModels as transient by default; singleton usage requires explicit justification.
|
|
12
|
+
- AI MUST place business logic in services only.
|
|
13
|
+
- AI MUST route data flow as API -> SQLite -> Service -> ViewModel -> View.
|
|
14
|
+
Prohibited:
|
|
15
|
+
- AI MUST NOT bypass these architecture constraints.
|
|
16
|
+
- AI MUST NOT invent alternative architectures, patterns, or folder models.
|
|
17
|
+
- AI MUST NOT place business logic in Views, code-behind, or ViewModels.
|
|
18
|
+
- AI MUST NOT call APIs or SQLite directly from Views or ViewModels.
|
|
19
|
+
- AI MUST NOT instantiate services with `new` outside composition root.
|
|
20
|
+
Patterns:
|
|
21
|
+
- Register dependencies in `MauiProgram.cs` or `Program.cs` only.
|
|
22
|
+
- Use constructor injection only.
|
|
23
|
+
- Use `InitializeAsync()` for runtime initialization.
|
|
24
|
+
- Keep feature modules self-contained under `Features/<FeatureName>/`.
|
|
25
|
+
Examples:
|
|
26
|
+
- `builder.Services.AddSingleton<IAuthService, AuthService>();`
|
|
27
|
+
- `await Shell.Current.GoToAsync("//dashboard");`
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Rule: Enforce CLI and Console Standards
|
|
2
|
+
Requirements:
|
|
3
|
+
- AI MUST follow all `.ai/rules/*.md` files when generating CLI code.
|
|
4
|
+
- AI MUST use Spectre.Console and Spectre.Console.Cli for CLI applications.
|
|
5
|
+
- AI MUST implement command handlers as command classes.
|
|
6
|
+
- AI MUST define arguments/options via `CommandSettings` classes.
|
|
7
|
+
- AI MUST register commands through `CommandApp` configuration.
|
|
8
|
+
- AI MUST use dependency injection for services and command dependencies.
|
|
9
|
+
- AI MUST return deterministic exit codes (`0` success, `1` runtime error, `2` invalid arguments, `3` configuration error).
|
|
10
|
+
- AI MUST support `--help` and `--version`.
|
|
11
|
+
- AI MUST use async command execution for I/O operations.
|
|
12
|
+
- AI MUST apply centralized logging with Serilog.
|
|
13
|
+
Prohibited:
|
|
14
|
+
- AI MUST NOT parse command-line args manually.
|
|
15
|
+
- AI MUST NOT instantiate services directly inside commands.
|
|
16
|
+
- AI MUST NOT use blocking synchronous I/O in command execution paths.
|
|
17
|
+
- AI MUST NOT use inconsistent command naming conventions.
|
|
18
|
+
- AI MUST NOT rely on interactive prompts for automation-only command paths.
|
|
19
|
+
Patterns:
|
|
20
|
+
- `public sealed class SyncCommand : AsyncCommand<SyncSettings>`
|
|
21
|
+
- `app.Configure(c => c.AddCommand<SyncCommand>("sync"));`
|
|
22
|
+
- `SyncCommand` depends on `ISyncService` via constructor injection.
|
|
23
|
+
- Output uses `AnsiConsole` rendering primitives.
|
|
24
|
+
Examples:
|
|
25
|
+
- `return 2; // invalid args`
|
|
26
|
+
- `await _syncService.RunAsync(cancellationToken);`
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Rule: Enforce Configuration and Secrets
|
|
2
|
+
Requirements:
|
|
3
|
+
- AI MUST follow all `.ai/rules/*.md` files for configuration and secret handling.
|
|
4
|
+
- AI MUST store non-sensitive settings in `appsettings.json`.
|
|
5
|
+
- AI MUST support environment-specific overrides (`appsettings.Development.json`, `appsettings.Production.json`).
|
|
6
|
+
- AI MUST centralize config access through a typed configuration service.
|
|
7
|
+
- AI MUST load and validate required configuration at startup (fail fast on missing required values).
|
|
8
|
+
- AI MUST store secrets in platform secure storage only.
|
|
9
|
+
- AI MUST use a dedicated secrets service abstraction.
|
|
10
|
+
- AI MUST inject configuration and secrets services via DI.
|
|
11
|
+
- AI MUST use strongly typed option models where applicable.
|
|
12
|
+
- AI MUST keep runtime configuration immutable after startup load unless explicitly designed for reload.
|
|
13
|
+
Prohibited:
|
|
14
|
+
- AI MUST NOT store secrets in `appsettings*.json`.
|
|
15
|
+
- AI MUST NOT commit secrets, credentials, or tokens to source control.
|
|
16
|
+
- AI MUST NOT access configuration via scattered magic-string lookups.
|
|
17
|
+
- AI MUST NOT log secret configuration values.
|
|
18
|
+
- AI MUST NOT bypass startup validation of required settings.
|
|
19
|
+
Patterns:
|
|
20
|
+
- `Configuration/AppConfigurationService.cs`
|
|
21
|
+
- `Configuration/SecretsService.cs`
|
|
22
|
+
- `builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: false);`
|
|
23
|
+
- Bind and validate options at startup.
|
|
24
|
+
Examples:
|
|
25
|
+
- `public string ApiBaseUrl => _config["Api:BaseUrl"]!;`
|
|
26
|
+
- `await SecureStorage.SetAsync("AuthToken", token);`
|