cfsa-antigravity 2.0.0 → 2.2.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/README.md +14 -0
- package/package.json +1 -1
- package/template/.agent/instructions/commands.md +8 -32
- package/template/.agent/instructions/example.md +21 -0
- package/template/.agent/instructions/patterns.md +3 -3
- package/template/.agent/instructions/tech-stack.md +71 -23
- package/template/.agent/instructions/workflow.md +12 -1
- package/template/.agent/rules/completion-checklist.md +6 -0
- package/template/.agent/rules/security-first.md +3 -3
- package/template/.agent/rules/vertical-slices.md +1 -1
- package/template/.agent/skill-library/MANIFEST.md +6 -0
- package/template/.agent/skill-library/stack/devops/git-advanced/SKILL.md +972 -0
- package/template/.agent/skill-library/stack/devops/git-workflow/SKILL.md +420 -0
- package/template/.agent/skills/api-versioning/SKILL.md +44 -298
- package/template/.agent/skills/api-versioning/references/typescript.md +157 -0
- package/template/.agent/skills/architecture-mapping/SKILL.md +13 -13
- package/template/.agent/skills/bootstrap-agents/SKILL.md +151 -152
- package/template/.agent/skills/clean-code/SKILL.md +64 -118
- package/template/.agent/skills/clean-code/references/typescript.md +126 -0
- package/template/.agent/skills/database-schema-design/SKILL.md +93 -317
- package/template/.agent/skills/database-schema-design/references/relational.md +228 -0
- package/template/.agent/skills/error-handling-patterns/SKILL.md +62 -557
- package/template/.agent/skills/error-handling-patterns/references/go.md +162 -0
- package/template/.agent/skills/error-handling-patterns/references/python.md +262 -0
- package/template/.agent/skills/error-handling-patterns/references/rust.md +112 -0
- package/template/.agent/skills/error-handling-patterns/references/typescript.md +178 -0
- package/template/.agent/skills/idea-extraction/SKILL.md +322 -224
- package/template/.agent/skills/logging-best-practices/SKILL.md +108 -767
- package/template/.agent/skills/logging-best-practices/references/go.md +49 -0
- package/template/.agent/skills/logging-best-practices/references/python.md +52 -0
- package/template/.agent/skills/logging-best-practices/references/typescript.md +215 -0
- package/template/.agent/skills/migration-management/SKILL.md +127 -311
- package/template/.agent/skills/migration-management/references/relational.md +214 -0
- package/template/.agent/skills/parallel-feature-development/SKILL.md +34 -43
- package/template/.agent/skills/pipeline-rubrics/references/be-rubric.md +1 -1
- package/template/.agent/skills/pipeline-rubrics/references/ia-rubric.md +2 -2
- package/template/.agent/skills/pipeline-rubrics/references/scoring.md +1 -1
- package/template/.agent/skills/pipeline-rubrics/references/vision-rubric.md +2 -1
- package/template/.agent/skills/prd-templates/SKILL.md +23 -6
- package/template/.agent/skills/prd-templates/references/be-spec-template.md +2 -2
- package/template/.agent/skills/prd-templates/references/decomposition-templates.md +2 -2
- package/template/.agent/skills/prd-templates/references/engineering-standards-template.md +2 -0
- package/template/.agent/skills/prd-templates/references/fe-spec-template.md +1 -1
- package/template/.agent/skills/prd-templates/references/fractal-cx-template.md +58 -0
- package/template/.agent/skills/prd-templates/references/fractal-feature-template.md +93 -0
- package/template/.agent/skills/prd-templates/references/fractal-node-index-template.md +55 -0
- package/template/.agent/skills/prd-templates/references/ideation-crosscut-template.md +26 -47
- package/template/.agent/skills/prd-templates/references/ideation-index-template.md +47 -31
- package/template/.agent/skills/prd-templates/references/operational-templates.md +1 -1
- package/template/.agent/skills/prd-templates/references/placeholder-workflow-mapping.md +50 -21
- package/template/.agent/skills/prd-templates/references/skill-loading-protocol.md +32 -0
- package/template/.agent/skills/prd-templates/references/slice-completion-gates.md +29 -0
- package/template/.agent/skills/prd-templates/references/spec-coverage-sweep.md +3 -3
- package/template/.agent/skills/prd-templates/references/tdd-testing-policy.md +39 -0
- package/template/.agent/skills/prd-templates/references/vision-template.md +8 -8
- package/template/.agent/skills/regex-patterns/SKILL.md +122 -540
- package/template/.agent/skills/regex-patterns/references/go.md +44 -0
- package/template/.agent/skills/regex-patterns/references/javascript.md +63 -0
- package/template/.agent/skills/regex-patterns/references/python.md +77 -0
- package/template/.agent/skills/regex-patterns/references/rust.md +43 -0
- package/template/.agent/skills/resolve-ambiguity/SKILL.md +1 -1
- package/template/.agent/skills/session-continuity/SKILL.md +11 -9
- package/template/.agent/skills/session-continuity/protocols/02-progress-generation.md +2 -2
- package/template/.agent/skills/session-continuity/protocols/04-pattern-extraction.md +1 -1
- package/template/.agent/skills/session-continuity/protocols/05-session-close.md +1 -1
- package/template/.agent/skills/session-continuity/protocols/09-parallel-claim.md +1 -1
- package/template/.agent/skills/session-continuity/protocols/10-placeholder-verification-gate.md +57 -78
- package/template/.agent/skills/session-continuity/protocols/11-parallel-synthesis.md +1 -1
- package/template/.agent/skills/spec-writing/SKILL.md +1 -1
- package/template/.agent/skills/tdd-workflow/SKILL.md +94 -317
- package/template/.agent/skills/tdd-workflow/references/typescript.md +231 -0
- package/template/.agent/skills/testing-strategist/SKILL.md +74 -687
- package/template/.agent/skills/testing-strategist/references/typescript.md +328 -0
- package/template/.agent/skills/workflow-automation/SKILL.md +62 -154
- package/template/.agent/skills/workflow-automation/references/inngest.md +88 -0
- package/template/.agent/skills/workflow-automation/references/temporal.md +64 -0
- package/template/.agent/workflows/bootstrap-agents-fill.md +85 -143
- package/template/.agent/workflows/bootstrap-agents-provision.md +90 -107
- package/template/.agent/workflows/create-prd-architecture.md +23 -16
- package/template/.agent/workflows/create-prd-compile.md +11 -12
- package/template/.agent/workflows/create-prd-design-system.md +1 -1
- package/template/.agent/workflows/create-prd-security.md +9 -11
- package/template/.agent/workflows/create-prd-stack.md +10 -4
- package/template/.agent/workflows/create-prd.md +9 -9
- package/template/.agent/workflows/decompose-architecture-structure.md +4 -6
- package/template/.agent/workflows/decompose-architecture-validate.md +18 -1
- package/template/.agent/workflows/decompose-architecture.md +18 -3
- package/template/.agent/workflows/evolve-contract.md +11 -11
- package/template/.agent/workflows/evolve-feature-classify.md +14 -6
- package/template/.agent/workflows/ideate-discover.md +72 -107
- package/template/.agent/workflows/ideate-extract.md +84 -63
- package/template/.agent/workflows/ideate-validate.md +26 -22
- package/template/.agent/workflows/ideate.md +9 -9
- package/template/.agent/workflows/implement-slice-setup.md +25 -23
- package/template/.agent/workflows/implement-slice-tdd.md +73 -89
- package/template/.agent/workflows/implement-slice.md +4 -4
- package/template/.agent/workflows/plan-phase-preflight.md +6 -2
- package/template/.agent/workflows/plan-phase-write.md +6 -8
- package/template/.agent/workflows/remediate-pipeline-assess.md +2 -1
- package/template/.agent/workflows/resolve-ambiguity.md +2 -2
- package/template/.agent/workflows/update-architecture-map.md +22 -5
- package/template/.agent/workflows/validate-phase-quality.md +155 -0
- package/template/.agent/workflows/validate-phase-readiness.md +167 -0
- package/template/.agent/workflows/validate-phase.md +19 -157
- package/template/.agent/workflows/verify-infrastructure.md +10 -10
- package/template/.agent/workflows/write-architecture-spec-design.md +23 -14
- package/template/.agent/workflows/write-be-spec-classify.md +25 -21
- package/template/.agent/workflows/write-be-spec.md +1 -1
- package/template/.agent/workflows/write-fe-spec-classify.md +6 -12
- package/template/.agent/workflows/write-fe-spec-write.md +1 -1
- package/template/AGENTS.md +6 -2
- package/template/GEMINI.md +5 -3
- package/template/docs/README.md +10 -10
- package/template/docs/kit-architecture.md +126 -33
- package/template/docs/plans/ideation/README.md +8 -3
- package/template/.agent/skills/prd-templates/references/ideation-domain-template.md +0 -55
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
# TypeScript Testing Patterns
|
|
2
|
+
|
|
3
|
+
Language-specific patterns for the `testing-strategist` skill. Read `SKILL.md` first for universal methodology.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Framework Stack
|
|
8
|
+
|
|
9
|
+
| Level | Tool | Purpose |
|
|
10
|
+
|-------|------|---------|
|
|
11
|
+
| Unit | **Jest** or **Vitest** | Test runner, assertions, mocking |
|
|
12
|
+
| Component | **React Testing Library** | User-centric component testing |
|
|
13
|
+
| Integration | **Supertest** | HTTP API testing |
|
|
14
|
+
| E2E | **Playwright** | Browser automation |
|
|
15
|
+
|
|
16
|
+
## Commands
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm test # Run tests
|
|
20
|
+
npm test -- --watch # Watch mode
|
|
21
|
+
npm run test:coverage # Coverage report
|
|
22
|
+
npx playwright test # E2E tests
|
|
23
|
+
open coverage/lcov-report/index.html # View HTML report
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Unit Test: Business Logic
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
// src/lib/pricing.ts
|
|
32
|
+
export function calculateTotal(items: { price: number; quantity: number }[]) {
|
|
33
|
+
return items.reduce((sum, item) => sum + item.price * item.quantity, 0)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function applyDiscount(total: number, discountPercent: number) {
|
|
37
|
+
if (discountPercent < 0 || discountPercent > 100) {
|
|
38
|
+
throw new Error('Invalid discount percentage')
|
|
39
|
+
}
|
|
40
|
+
return total * (1 - discountPercent / 100)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// src/lib/pricing.test.ts
|
|
44
|
+
import { calculateTotal, applyDiscount } from './pricing'
|
|
45
|
+
|
|
46
|
+
describe('calculateTotal', () => {
|
|
47
|
+
it('calculates total for single item', () => {
|
|
48
|
+
const items = [{ price: 10, quantity: 2 }]
|
|
49
|
+
expect(calculateTotal(items)).toBe(20)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('calculates total for multiple items', () => {
|
|
53
|
+
const items = [
|
|
54
|
+
{ price: 10, quantity: 2 },
|
|
55
|
+
{ price: 5, quantity: 3 }
|
|
56
|
+
]
|
|
57
|
+
expect(calculateTotal(items)).toBe(35)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('returns 0 for empty array', () => {
|
|
61
|
+
expect(calculateTotal([])).toBe(0)
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('applyDiscount', () => {
|
|
66
|
+
it('applies discount correctly', () => {
|
|
67
|
+
expect(applyDiscount(100, 20)).toBe(80)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('throws error for invalid discount', () => {
|
|
71
|
+
expect(() => applyDiscount(100, -10)).toThrow('Invalid discount')
|
|
72
|
+
expect(() => applyDiscount(100, 150)).toThrow('Invalid discount')
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Unit Test: React Components
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import { render, screen, fireEvent } from '@testing-library/react'
|
|
81
|
+
import { Button } from './Button'
|
|
82
|
+
|
|
83
|
+
describe('Button', () => {
|
|
84
|
+
it('renders with correct text', () => {
|
|
85
|
+
render(<Button>Click me</Button>)
|
|
86
|
+
expect(screen.getByText('Click me')).toBeInTheDocument()
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('applies primary variant by default', () => {
|
|
90
|
+
render(<Button>Click me</Button>)
|
|
91
|
+
expect(screen.getByRole('button')).toHaveClass('btn-primary')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('calls onClick when clicked', () => {
|
|
95
|
+
const handleClick = jest.fn()
|
|
96
|
+
render(<Button onClick={handleClick}>Click me</Button>)
|
|
97
|
+
|
|
98
|
+
fireEvent.click(screen.getByRole('button'))
|
|
99
|
+
expect(handleClick).toHaveBeenCalledTimes(1)
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Unit Test: Custom Hooks
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
import { renderHook, act } from '@testing-library/react'
|
|
108
|
+
import { useCounter } from './useCounter'
|
|
109
|
+
|
|
110
|
+
describe('useCounter', () => {
|
|
111
|
+
it('initializes with default value', () => {
|
|
112
|
+
const { result } = renderHook(() => useCounter())
|
|
113
|
+
expect(result.current.count).toBe(0)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('increments count', () => {
|
|
117
|
+
const { result } = renderHook(() => useCounter())
|
|
118
|
+
act(() => { result.current.increment() })
|
|
119
|
+
expect(result.current.count).toBe(1)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('resets to initial value', () => {
|
|
123
|
+
const { result } = renderHook(() => useCounter(10))
|
|
124
|
+
act(() => {
|
|
125
|
+
result.current.increment()
|
|
126
|
+
result.current.reset()
|
|
127
|
+
})
|
|
128
|
+
expect(result.current.count).toBe(10)
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Integration Test: API Routes
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
import { testClient } from '@/lib/test-utils'
|
|
139
|
+
|
|
140
|
+
describe('POST /api/posts', () => {
|
|
141
|
+
beforeEach(async () => {
|
|
142
|
+
await db.post.deleteMany()
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('creates a new post', async () => {
|
|
146
|
+
const response = await testClient
|
|
147
|
+
.post('/api/posts')
|
|
148
|
+
.set('Authorization', `Bearer ${authToken}`)
|
|
149
|
+
.send({ title: 'Test Post', content: 'This is a test post' })
|
|
150
|
+
|
|
151
|
+
expect(response.status).toBe(201)
|
|
152
|
+
expect(response.body).toMatchObject({
|
|
153
|
+
title: 'Test Post',
|
|
154
|
+
content: 'This is a test post'
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
// Verify in database
|
|
158
|
+
const posts = await db.post.findMany()
|
|
159
|
+
expect(posts).toHaveLength(1)
|
|
160
|
+
expect(posts[0].title).toBe('Test Post')
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('returns 400 for invalid data', async () => {
|
|
164
|
+
const response = await testClient
|
|
165
|
+
.post('/api/posts')
|
|
166
|
+
.set('Authorization', `Bearer ${authToken}`)
|
|
167
|
+
.send({ title: '' })
|
|
168
|
+
|
|
169
|
+
expect(response.status).toBe(400)
|
|
170
|
+
expect(response.body.errors).toBeDefined()
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('returns 401 for unauthenticated request', async () => {
|
|
174
|
+
const response = await testClient.post('/api/posts')
|
|
175
|
+
.send({ title: 'Test', content: 'Test' })
|
|
176
|
+
|
|
177
|
+
expect(response.status).toBe(401)
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Integration Test: Database Operations
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
describe('userRepository', () => {
|
|
186
|
+
beforeEach(async () => { await db.user.deleteMany() })
|
|
187
|
+
afterAll(async () => { await db.$disconnect() })
|
|
188
|
+
|
|
189
|
+
describe('createUser', () => {
|
|
190
|
+
it('creates user with hashed password', async () => {
|
|
191
|
+
const user = await createUser({
|
|
192
|
+
email: 'test@example.com',
|
|
193
|
+
password: 'password123'
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
expect(user.email).toBe('test@example.com')
|
|
197
|
+
expect(user.password).not.toBe('password123')
|
|
198
|
+
expect(user.password).toMatch(/^\$2[aby]/) // bcrypt hash
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('throws error for duplicate email', async () => {
|
|
202
|
+
await createUser({ email: 'test@example.com', password: 'pass' })
|
|
203
|
+
await expect(createUser({ email: 'test@example.com', password: 'pass' }))
|
|
204
|
+
.rejects.toThrow()
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
})
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## E2E Test: Playwright
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
import { test, expect } from '@playwright/test'
|
|
216
|
+
|
|
217
|
+
test.describe('Authentication', () => {
|
|
218
|
+
test('user can sign up and log in', async ({ page }) => {
|
|
219
|
+
await page.goto('/signup')
|
|
220
|
+
await page.fill('input[name="email"]', 'test@example.com')
|
|
221
|
+
await page.fill('input[name="password"]', 'SecurePass123!')
|
|
222
|
+
await page.fill('input[name="confirmPassword"]', 'SecurePass123!')
|
|
223
|
+
await page.click('button[type="submit"]')
|
|
224
|
+
|
|
225
|
+
await expect(page).toHaveURL('/dashboard')
|
|
226
|
+
await expect(page.locator('h1')).toContainText('Welcome')
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
test('shows error for invalid credentials', async ({ page }) => {
|
|
230
|
+
await page.goto('/login')
|
|
231
|
+
await page.fill('input[name="email"]', 'wrong@example.com')
|
|
232
|
+
await page.fill('input[name="password"]', 'wrongpassword')
|
|
233
|
+
await page.click('button[type="submit"]')
|
|
234
|
+
|
|
235
|
+
await expect(page.locator('[role="alert"]')).toContainText('Invalid credentials')
|
|
236
|
+
await expect(page).toHaveURL('/login')
|
|
237
|
+
})
|
|
238
|
+
})
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Mocking
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
// Mock external API
|
|
247
|
+
import { fetchUserData } from '@/lib/api'
|
|
248
|
+
|
|
249
|
+
jest.mock('@/lib/api')
|
|
250
|
+
const mockFetchUserData = fetchUserData as jest.MockedFunction<typeof fetchUserData>
|
|
251
|
+
|
|
252
|
+
it('displays user data', async () => {
|
|
253
|
+
mockFetchUserData.mockResolvedValue({
|
|
254
|
+
id: '1', name: 'John Doe', email: 'john@example.com'
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
render(<UserProfile userId="1" />)
|
|
258
|
+
|
|
259
|
+
await waitFor(() => {
|
|
260
|
+
expect(screen.getByText('John Doe')).toBeInTheDocument()
|
|
261
|
+
})
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
// Mock Date
|
|
265
|
+
beforeAll(() => {
|
|
266
|
+
jest.useFakeTimers()
|
|
267
|
+
jest.setSystemTime(new Date('2024-01-01'))
|
|
268
|
+
})
|
|
269
|
+
afterAll(() => { jest.useRealTimers() })
|
|
270
|
+
|
|
271
|
+
// Mock Math.random
|
|
272
|
+
const mockRandom = jest.spyOn(Math, 'random')
|
|
273
|
+
mockRandom.mockReturnValue(0.5)
|
|
274
|
+
// ... assertions ...
|
|
275
|
+
mockRandom.mockRestore()
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## Coverage Configuration
|
|
281
|
+
|
|
282
|
+
```javascript
|
|
283
|
+
// jest.config.js
|
|
284
|
+
module.exports = {
|
|
285
|
+
collectCoverageFrom: [
|
|
286
|
+
'src/**/*.{ts,tsx}',
|
|
287
|
+
'!src/**/*.d.ts',
|
|
288
|
+
'!src/**/*.stories.tsx',
|
|
289
|
+
'!src/types/**'
|
|
290
|
+
],
|
|
291
|
+
coverageThresholds: {
|
|
292
|
+
global: {
|
|
293
|
+
branches: 70, functions: 70, lines: 70, statements: 70
|
|
294
|
+
},
|
|
295
|
+
'./src/lib/auth/**': {
|
|
296
|
+
branches: 90, functions: 90, lines: 90
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
## CI/CD
|
|
303
|
+
|
|
304
|
+
```yaml
|
|
305
|
+
- name: Run Tests
|
|
306
|
+
run: npm test -- --coverage
|
|
307
|
+
- name: Upload Coverage
|
|
308
|
+
uses: codecov/codecov-action@v3
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## Assertion Examples
|
|
312
|
+
|
|
313
|
+
### ❌ Shallow (BANNED)
|
|
314
|
+
```typescript
|
|
315
|
+
expect(result).toBeDefined()
|
|
316
|
+
expect(result).toBeTruthy()
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### ✅ Deep (REQUIRED)
|
|
320
|
+
```typescript
|
|
321
|
+
expect(result).toEqual({
|
|
322
|
+
id: expect.any(String),
|
|
323
|
+
name: 'Test',
|
|
324
|
+
items: expect.arrayContaining([
|
|
325
|
+
expect.objectContaining({ amount: 100 })
|
|
326
|
+
])
|
|
327
|
+
})
|
|
328
|
+
```
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: workflow-automation
|
|
3
|
-
description: "Architect durable, event-driven workflows using
|
|
3
|
+
description: "Architect durable, event-driven workflows using step functions, retry strategies, idempotency, fan-out patterns, and the critical differences between orchestration approaches."
|
|
4
4
|
version: 2.0.0
|
|
5
|
-
source: self
|
|
6
|
-
date_added: "2026-02-27"
|
|
7
|
-
date_rewritten: "2026-03-14"
|
|
8
5
|
---
|
|
9
6
|
|
|
10
7
|
# Workflow Automation
|
|
@@ -21,31 +18,39 @@ You are a workflow architect who understands the fundamental tradeoff: **simplic
|
|
|
21
18
|
|
|
22
19
|
## When NOT to Use
|
|
23
20
|
|
|
24
|
-
- Simple fire-and-forget tasks (use a queue or
|
|
21
|
+
- Simple fire-and-forget tasks (use a queue or a delay)
|
|
25
22
|
- Synchronous request-response flows
|
|
26
23
|
- Tasks under 100ms that don't involve external services
|
|
27
24
|
|
|
25
|
+
## Ecosystem-Specific References
|
|
26
|
+
|
|
27
|
+
After reading the methodology below, read the reference matching your orchestration platform:
|
|
28
|
+
|
|
29
|
+
| Platform | Reference | Best For |
|
|
30
|
+
|----------|-----------|----------|
|
|
31
|
+
| Inngest | `references/inngest.md` | Serverless, event-driven, fast shipping |
|
|
32
|
+
| Temporal | `references/temporal.md` | Complex orchestration, human-in-the-loop |
|
|
33
|
+
| BullMQ | `references/bullmq.md` | Simple job queues, rate limiting, Redis stack |
|
|
34
|
+
|
|
28
35
|
---
|
|
29
36
|
|
|
30
37
|
## Core Concept: Durable Execution
|
|
31
38
|
|
|
32
|
-
|
|
39
|
+
**Wrap each unit of work in a step, so the system can retry, resume, and checkpoint independently.**
|
|
33
40
|
|
|
34
|
-
Without durable execution:
|
|
35
|
-
```
|
|
36
|
-
fetch data → process → save → send email
|
|
37
|
-
↑ server crashes here
|
|
38
|
-
↓ entire pipeline reruns from scratch
|
|
39
|
-
↓ customer gets duplicate email
|
|
40
41
|
```
|
|
42
|
+
Without durable execution:
|
|
43
|
+
fetch data → process → save → send email
|
|
44
|
+
↑ server crashes here
|
|
45
|
+
↓ entire pipeline reruns from scratch
|
|
46
|
+
↓ customer gets duplicate email
|
|
41
47
|
|
|
42
48
|
With durable execution:
|
|
43
|
-
|
|
44
|
-
step("
|
|
45
|
-
step("
|
|
46
|
-
step("save") ←
|
|
47
|
-
step("
|
|
48
|
-
step("email") ✓ runs once
|
|
49
|
+
step("fetch") ✓ saved
|
|
50
|
+
step("process") ✓ saved
|
|
51
|
+
step("save") ← server crashes here
|
|
52
|
+
step("save") ← retries ONLY this step
|
|
53
|
+
step("email") ✓ runs once
|
|
49
54
|
```
|
|
50
55
|
|
|
51
56
|
---
|
|
@@ -57,17 +62,17 @@ step("email") ✓ runs once
|
|
|
57
62
|
| **Complexity** | Low (serverless functions) | High (workers + server) | Medium (Redis-backed) |
|
|
58
63
|
| **Durability** | Steps checkpointed | Full workflow replay | Job-level retry |
|
|
59
64
|
| **Infrastructure** | Managed or self-hosted | Requires Temporal Server | Requires Redis |
|
|
60
|
-
| **Best for** | Serverless,
|
|
65
|
+
| **Best for** | Serverless, event-driven | Complex orchestration | Simple job queues |
|
|
61
66
|
| **Learning curve** | 1-2 days | 1-2 weeks | 1-2 days |
|
|
62
67
|
| **Language support** | TS, Python, Go | TS, Go, Java, Python, .NET | TS/JS only |
|
|
63
68
|
|
|
64
69
|
### Decision Rules
|
|
65
70
|
|
|
66
71
|
1. **Serverless + event-driven?** → Inngest
|
|
67
|
-
2. **Complex orchestration, human-in-the-loop, multi-day
|
|
68
|
-
3. **Simple job queue
|
|
72
|
+
2. **Complex orchestration, human-in-the-loop, multi-day?** → Temporal
|
|
73
|
+
3. **Simple job queue, Redis already in stack?** → BullMQ
|
|
69
74
|
4. **Team of 1-3, shipping fast?** → Inngest
|
|
70
|
-
5. **Enterprise, compliance-heavy,
|
|
75
|
+
5. **Enterprise, compliance-heavy, Java/.NET stack?** → Temporal
|
|
71
76
|
|
|
72
77
|
---
|
|
73
78
|
|
|
@@ -75,85 +80,28 @@ step("email") ✓ runs once
|
|
|
75
80
|
|
|
76
81
|
Each step depends on the previous result. Steps checkpoint independently.
|
|
77
82
|
|
|
78
|
-
### Inngest
|
|
79
|
-
|
|
80
|
-
```typescript
|
|
81
|
-
const syncUser = inngest.createFunction(
|
|
82
|
-
{ id: "sync-user-data" },
|
|
83
|
-
{ event: "user/signup.completed" },
|
|
84
|
-
async ({ event, step }) => {
|
|
85
|
-
const user = await step.run("create-db-record", async () => {
|
|
86
|
-
return db.users.create({ email: event.data.email });
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
const stripeCustomer = await step.run("create-stripe-customer", async () => {
|
|
90
|
-
return stripe.customers.create({ email: user.email });
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
await step.run("send-welcome-email", async () => {
|
|
94
|
-
return email.send({ to: user.email, template: "welcome" });
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
);
|
|
98
83
|
```
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
```typescript
|
|
103
|
-
// activities.ts
|
|
104
|
-
export async function createDbRecord(email: string): Promise<User> {
|
|
105
|
-
return db.users.create({ email });
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export async function createStripeCustomer(email: string): Promise<Customer> {
|
|
109
|
-
return stripe.customers.create({ email });
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// workflows.ts
|
|
113
|
-
const { createDbRecord, createStripeCustomer } = proxyActivities<typeof activities>({
|
|
114
|
-
startToCloseTimeout: "30 seconds",
|
|
115
|
-
retry: { initialInterval: "1s", maximumAttempts: 3 },
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
export async function syncUserWorkflow(email: string): Promise<void> {
|
|
119
|
-
const user = await createDbRecord(email);
|
|
120
|
-
const customer = await createStripeCustomer(user.email);
|
|
121
|
-
}
|
|
84
|
+
step("create-db-record") → returns user
|
|
85
|
+
step("create-stripe-customer") → uses user.email
|
|
86
|
+
step("send-welcome-email") → uses user.email
|
|
122
87
|
```
|
|
123
88
|
|
|
89
|
+
If step 2 fails, only step 2 retries. Steps 1 and 3 are not re-executed.
|
|
90
|
+
|
|
124
91
|
---
|
|
125
92
|
|
|
126
93
|
## Pattern 2: Fan-Out (Parallel Execution)
|
|
127
94
|
|
|
128
95
|
One event triggers multiple independent workflows. Each runs and retries independently.
|
|
129
96
|
|
|
130
|
-
### Inngest
|
|
131
|
-
|
|
132
|
-
```typescript
|
|
133
|
-
// Each function subscribes to the same event — runs independently
|
|
134
|
-
const sendWelcome = inngest.createFunction(
|
|
135
|
-
{ id: "send-welcome-email" },
|
|
136
|
-
{ event: "user/signup.completed" },
|
|
137
|
-
async ({ event, step }) => {
|
|
138
|
-
await step.run("send", async () => {
|
|
139
|
-
await email.send({ to: event.data.email, template: "welcome" });
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
const startTrial = inngest.createFunction(
|
|
145
|
-
{ id: "start-stripe-trial" },
|
|
146
|
-
{ event: "user/signup.completed" },
|
|
147
|
-
async ({ event, step }) => {
|
|
148
|
-
await step.run("create-trial", async () => {
|
|
149
|
-
await stripe.subscriptions.create({
|
|
150
|
-
customer: event.data.stripeId,
|
|
151
|
-
trial_period_days: 14,
|
|
152
|
-
});
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
);
|
|
156
97
|
```
|
|
98
|
+
Event: user/signup.completed
|
|
99
|
+
→ Workflow A: send-welcome-email (independent)
|
|
100
|
+
→ Workflow B: create-stripe-trial (independent)
|
|
101
|
+
→ Workflow C: provision-workspace (independent)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
If Workflow B fails, Workflows A and C are unaffected.
|
|
157
105
|
|
|
158
106
|
---
|
|
159
107
|
|
|
@@ -161,72 +109,32 @@ const startTrial = inngest.createFunction(
|
|
|
161
109
|
|
|
162
110
|
Prevent duplicate execution when the same event fires twice.
|
|
163
111
|
|
|
164
|
-
|
|
165
|
-
const processPayment = inngest.createFunction(
|
|
166
|
-
{
|
|
167
|
-
id: "process-payment",
|
|
168
|
-
// Only runs once per unique orderId within 24h
|
|
169
|
-
idempotency: "event.data.orderId",
|
|
170
|
-
},
|
|
171
|
-
{ event: "order/payment.requested" },
|
|
172
|
-
async ({ event, step }) => {
|
|
173
|
-
await step.run("charge", async () => {
|
|
174
|
-
return stripe.charges.create({
|
|
175
|
-
amount: event.data.amount,
|
|
176
|
-
idempotencyKey: event.data.orderId, // Stripe-level idempotency too
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
);
|
|
181
|
-
```
|
|
112
|
+
**Rule:** Idempotency at the workflow level AND the external call level. Belt and suspenders.
|
|
182
113
|
|
|
183
|
-
**
|
|
114
|
+
- **Workflow level:** Deduplicate by event ID or business key (e.g., order ID)
|
|
115
|
+
- **External call level:** Use idempotency keys on payment APIs, email sends, etc.
|
|
184
116
|
|
|
185
117
|
---
|
|
186
118
|
|
|
187
119
|
## Pattern 4: Scheduled/Recurring
|
|
188
120
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
{ id: "daily-metrics-report" },
|
|
194
|
-
{ cron: "0 9 * * *" }, // 9am daily
|
|
195
|
-
async ({ step }) => {
|
|
196
|
-
const metrics = await step.run("fetch-metrics", fetchDailyMetrics);
|
|
197
|
-
await step.run("send-report", async () => sendSlackReport(metrics));
|
|
198
|
-
}
|
|
199
|
-
);
|
|
200
|
-
```
|
|
121
|
+
Cron-triggered workflows with observability:
|
|
122
|
+
- Scheduled trigger (e.g., daily at 9am)
|
|
123
|
+
- Steps checkpoint independently
|
|
124
|
+
- Failed runs are visible, retriable, and alertable
|
|
201
125
|
|
|
202
126
|
---
|
|
203
127
|
|
|
204
|
-
## Pattern 5: Human-in-the-Loop
|
|
128
|
+
## Pattern 5: Human-in-the-Loop
|
|
205
129
|
|
|
206
130
|
Workflows that pause and wait for external input — approval flows, multi-day processes.
|
|
207
131
|
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
setHandler(approvalSignal, (isApproved) => {
|
|
216
|
-
approved = isApproved;
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
// Workflow sleeps until signal received or 72h timeout
|
|
220
|
-
const gotApproval = await condition(() => approved !== undefined, "72 hours");
|
|
221
|
-
|
|
222
|
-
if (!gotApproval || !approved) {
|
|
223
|
-
return "Purchase denied or timed out";
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Continue with purchase...
|
|
228
|
-
return "Purchase completed";
|
|
229
|
-
}
|
|
132
|
+
```
|
|
133
|
+
workflow: purchaseWorkflow(amount)
|
|
134
|
+
if amount > threshold:
|
|
135
|
+
pause and wait for approval signal (up to 72h)
|
|
136
|
+
if no approval → deny
|
|
137
|
+
continue with purchase
|
|
230
138
|
```
|
|
231
139
|
|
|
232
140
|
---
|
|
@@ -235,19 +143,19 @@ export async function purchaseWorkflow(amount: number): Promise<string> {
|
|
|
235
143
|
|
|
236
144
|
| Issue | Severity | Rule |
|
|
237
145
|
|-------|----------|------|
|
|
238
|
-
| No idempotency keys on external calls | Critical | ALWAYS use idempotency keys for payments, emails,
|
|
239
|
-
| Side effects in workflow code (Temporal) | Critical | Workflows must be deterministic — no I/O, no random, no
|
|
240
|
-
| Giant payloads in step results | High | Steps
|
|
241
|
-
| No timeouts on activities | High | ALWAYS set
|
|
242
|
-
| Linear retry without backoff | Medium | ALWAYS use exponential backoff
|
|
243
|
-
| Missing dead letter handling | High | Failed-after-all-retries jobs need a destination
|
|
244
|
-
| Monolithic workflows | Medium | Break workflows >10 steps into child workflows
|
|
146
|
+
| No idempotency keys on external calls | Critical | ALWAYS use idempotency keys for payments, emails, mutations |
|
|
147
|
+
| Side effects in workflow code (Temporal) | Critical | Workflows must be deterministic — no I/O, no random, no clock |
|
|
148
|
+
| Giant payloads in step results | High | Steps serialize data — keep under 256KB |
|
|
149
|
+
| No timeouts on activities | High | ALWAYS set timeouts — default infinite hangs forever |
|
|
150
|
+
| Linear retry without backoff | Medium | ALWAYS use exponential backoff |
|
|
151
|
+
| Missing dead letter handling | High | Failed-after-all-retries jobs need a destination |
|
|
152
|
+
| Monolithic workflows | Medium | Break workflows >10 steps into child workflows |
|
|
245
153
|
|
|
246
154
|
## Anti-Patterns
|
|
247
155
|
|
|
248
156
|
| Don't | Do |
|
|
249
157
|
|-------|-----|
|
|
250
|
-
| `setTimeout(fn, 3600000)` for delays |
|
|
158
|
+
| `setTimeout(fn, 3600000)` for delays | Platform sleep — survives restarts |
|
|
251
159
|
| Global variables for workflow state | Step results and explicit state passing |
|
|
252
160
|
| Retry loops in application code | Platform-level retry policies |
|
|
253
161
|
| Polling in a loop for completion | Event-driven triggers or workflow signals |
|
|
@@ -255,4 +163,4 @@ export async function purchaseWorkflow(amount: number): Promise<string> {
|
|
|
255
163
|
|
|
256
164
|
## Related Skills
|
|
257
165
|
|
|
258
|
-
Works with: `error-handling-patterns`, `deployment-procedures
|
|
166
|
+
Works with: `error-handling-patterns`, `deployment-procedures`
|