Skip to content

Commit 8cfa234

Browse files
committed
add docs for conditional steps
1 parent 44482fc commit 8cfa234

9 files changed

Lines changed: 1190 additions & 65 deletions

File tree

pkgs/website/astro.config.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,10 @@ export default defineConfig({
271271
},
272272
],
273273
},
274+
{
275+
label: 'Conditional Steps',
276+
autogenerate: { directory: 'build/conditional-steps/' },
277+
},
274278
{
275279
label: 'Starting Flows',
276280
autogenerate: { directory: 'build/starting-flows/' },

pkgs/website/src/assets/pgflow-theme.d2

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ classes: {
6969
style.stroke: "#e85c5c"
7070
}
7171

72-
# Step state classes (created, started, completed, failed)
72+
# Step state classes (created, started, completed, failed, skipped)
7373
step_created: {
7474
style.fill: "#95a0a3"
7575
style.stroke: "#4a5759"
@@ -86,6 +86,11 @@ classes: {
8686
style.fill: "#a33636"
8787
style.stroke: "#e85c5c"
8888
}
89+
step_skipped: {
90+
style.fill: "#4a5759"
91+
style.stroke: "#6b7a7d"
92+
style.stroke-dash: 3
93+
}
8994

9095
# Task state classes (queued, completed, failed)
9196
task_queued: {
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
---
2+
title: Error Handling
3+
description: Handle step failures gracefully with retriesExhausted option.
4+
sidebar:
5+
order: 3
6+
---
7+
8+
import { Aside, CardGrid, LinkCard } from '@astrojs/starlight/components';
9+
10+
The `retriesExhausted` option controls what happens when a step fails after exhausting all retry attempts. Instead of failing the entire run, you can mark the step as skipped and continue.
11+
12+
## Default Behavior
13+
14+
By default, when a step fails after all retries, the entire run fails:
15+
16+
```typescript
17+
.step({
18+
slug: 'sendNotification',
19+
maxAttempts: 3,
20+
// retriesExhausted: 'fail' // This is the default
21+
}, async (input) => {
22+
await sendEmail(input.run.email); // If this fails 3 times, run fails
23+
})
24+
```
25+
26+
## Graceful Failure with skip
27+
28+
Use `retriesExhausted: 'skip'` for non-critical steps that shouldn't block the workflow:
29+
30+
```typescript
31+
.step({
32+
slug: 'sendWelcomeEmail',
33+
maxAttempts: 3,
34+
retriesExhausted: 'skip', // If email fails, continue anyway
35+
}, async (input) => {
36+
return await sendEmail(input.run.email);
37+
})
38+
.step({
39+
slug: 'createAccount',
40+
dependsOn: ['sendWelcomeEmail'],
41+
}, async (input) => {
42+
// This runs even if email failed
43+
// input.sendWelcomeEmail may be undefined
44+
const emailSent = input.sendWelcomeEmail !== undefined;
45+
return { accountId: await createUser(input.run), emailSent };
46+
})
47+
```
48+
49+
<Aside type="tip">
50+
Use `retriesExhausted: 'skip'` for:
51+
- Notifications (email, SMS, push)
52+
- Analytics and tracking
53+
- Optional enrichment
54+
- Logging to external services
55+
56+
These shouldn't block your core business logic.
57+
58+
</Aside>
59+
60+
## Cascade Failure with skip-cascade
61+
62+
Use `retriesExhausted: 'skip-cascade'` when a failed step should skip its entire dependency chain:
63+
64+
```typescript
65+
.step({
66+
slug: 'fetchExternalData',
67+
maxAttempts: 5,
68+
retriesExhausted: 'skip-cascade', // Skip this + dependents on failure
69+
}, async (input) => {
70+
return await fetchFromUnreliableAPI(input.run.id);
71+
})
72+
.step({
73+
slug: 'processExternalData',
74+
dependsOn: ['fetchExternalData'],
75+
}, processHandler) // Also skipped if fetch fails
76+
.step({
77+
slug: 'useLocalFallback',
78+
// No dependency on external data
79+
}, fallbackHandler) // This always runs
80+
```
81+
82+
```d2 width="700" pad="20"
83+
...@../../../../assets/pgflow-theme.d2
84+
85+
direction: right
86+
87+
fetch: "fetchExternalData"
88+
fetch.class: step_skipped
89+
90+
process: "processExternalData"
91+
process.class: step_skipped
92+
93+
fallback: "useLocalFallback"
94+
fallback.class: step_completed
95+
96+
fetch -> process: "cascade skip" {
97+
style.stroke-dash: 3
98+
}
99+
100+
note: "fetch failed after 5 attempts\nprocess cascaded to skipped\nfallback runs independently" {
101+
style.fill: transparent
102+
style.stroke: transparent
103+
}
104+
```
105+
106+
## Type Safety
107+
108+
Like `whenUnmet`, the `retriesExhausted` option affects TypeScript types:
109+
110+
| Mode | Dependent Input Type |
111+
| -------------- | --------------------------- |
112+
| `fail` | `T` (required) |
113+
| `skip` | `T \| undefined` (optional) |
114+
| `skip-cascade` | `T` (required) |
115+
116+
```typescript
117+
// With retriesExhausted: 'skip'
118+
.step({
119+
slug: 'optional',
120+
retriesExhausted: 'skip',
121+
}, () => ({ data: 'value' }))
122+
123+
.step({
124+
slug: 'consumer',
125+
dependsOn: ['optional'],
126+
}, async (input) => {
127+
// TypeScript knows input.optional may be undefined
128+
if (input.optional) {
129+
return processData(input.optional.data);
130+
}
131+
return processWithoutData();
132+
})
133+
```
134+
135+
## TYPE_VIOLATION: The Exception
136+
137+
Some errors indicate programming mistakes that should never be silently ignored. These **TYPE_VIOLATION** errors always fail the run, regardless of `retriesExhausted`:
138+
139+
```typescript
140+
// This will ALWAYS fail the run, even with retriesExhausted: 'skip'
141+
.step({
142+
slug: 'fetchItems',
143+
retriesExhausted: 'skip',
144+
}, () => "not an array") // Returns string instead of array!
145+
.map({
146+
slug: 'processItems',
147+
array: 'fetchItems', // Expects array, gets string
148+
}, (item) => item * 2)
149+
```
150+
151+
<Aside type="caution" title="TYPE_VIOLATION Errors">
152+
These programming errors ALWAYS fail the run:
153+
- A step returns a non-array value for a dependent `.map()` step
154+
- Root `.map()` receives non-array flow input
155+
156+
These are bugs in your code, not runtime conditions. Fix the handler or input validation.
157+
158+
</Aside>
159+
160+
## Skip Reason for Failed Steps
161+
162+
When `retriesExhausted: 'skip'` triggers, the step gets:
163+
164+
- `status: 'skipped'`
165+
- `skip_reason: 'handler_failed'`
166+
- Original error preserved in `error_message`
167+
168+
Query failed-but-skipped steps:
169+
170+
```sql
171+
SELECT step_slug, error_message, skip_reason
172+
FROM pgflow.step_states
173+
WHERE run_id = 'your-run-id'
174+
AND skip_reason = 'handler_failed';
175+
```
176+
177+
## Combining with Conditions
178+
179+
You can use both condition options and failure handling on the same step:
180+
181+
```typescript
182+
.step({
183+
slug: 'premiumNotification',
184+
if: { plan: 'premium' }, // Only for premium users
185+
whenUnmet: 'skip', // Skip if not premium
186+
maxAttempts: 3,
187+
retriesExhausted: 'skip', // Skip if notification fails
188+
}, async (input) => {
189+
return await sendPremiumEmail(input.run.email);
190+
})
191+
```
192+
193+
The step can be skipped for two different reasons:
194+
195+
1. `condition_unmet` - User isn't premium
196+
2. `handler_failed` - Email service failed after 3 attempts
197+
198+
## Best Practices
199+
200+
### Do Use skip for Side Effects
201+
202+
```typescript
203+
// Good: Notifications shouldn't block core logic
204+
.step({
205+
slug: 'sendSlackNotification',
206+
maxAttempts: 2,
207+
retriesExhausted: 'skip',
208+
}, notifySlack)
209+
```
210+
211+
### Don't Use skip for Critical Steps
212+
213+
```typescript
214+
// Bad: Payment processing should never silently fail
215+
.step({
216+
slug: 'chargePayment',
217+
maxAttempts: 3,
218+
retriesExhausted: 'skip', // Don't do this!
219+
}, chargeCard)
220+
```
221+
222+
### Use skip-cascade for Feature Branches
223+
224+
```typescript
225+
// Good: If enrichment source is down, skip all enrichment
226+
.step({
227+
slug: 'fetchEnrichmentData',
228+
retriesExhausted: 'skip-cascade',
229+
}, fetchEnrichment)
230+
.step({
231+
slug: 'applyEnrichment',
232+
dependsOn: ['fetchEnrichmentData'],
233+
}, applyEnrichment) // Skipped if fetch failed
234+
```
235+
236+
## Learn More
237+
238+
<CardGrid>
239+
<LinkCard
240+
title="Retrying Steps"
241+
href="/build/retrying-steps/"
242+
description="Configure retry policies and understand failure types"
243+
/>
244+
<LinkCard
245+
title="Skip Modes"
246+
href="/build/conditional-steps/skip-modes/"
247+
description="Understand fail, skip, and skip-cascade behaviors"
248+
/>
249+
<LinkCard
250+
title="Pattern Matching"
251+
href="/build/conditional-steps/pattern-matching/"
252+
description="Use if/ifNot conditions with JSON containment patterns"
253+
/>
254+
</CardGrid>

0 commit comments

Comments
 (0)