Skip to content

Commit 320a106

Browse files
authored
docs: add conditional steps documentation and skipped step styling (#594)
# Add Conditional Steps and Error Handling This PR introduces comprehensive support for conditional step execution and graceful error handling in pgflow. The new features allow workflows to adapt to runtime conditions and handle failures without stopping the entire run. ## Key Features - **Pattern Matching**: Control step execution with `if`/`ifNot` conditions using PostgreSQL's JSON containment operator - **Skip Modes**: Configure what happens when conditions aren't met with `fail`, `skip`, or `skip-cascade` options - **Error Handling**: Gracefully handle step failures with `retriesExhausted: 'skip'` for non-critical steps - **Type Safety**: TypeScript types reflect optional dependencies when steps might be skipped ## Visual Representation Added a new `step_skipped` style to the D2 theme for visualizing skipped steps in flow diagrams. ## Documentation Added comprehensive documentation with: - Overview of conditional execution and error handling - Pattern matching syntax and examples - Skip mode behaviors and use cases - Error handling best practices - Type safety considerations - Visual diagrams showing different execution paths These features enable more resilient workflows where non-critical steps can fail without stopping the entire process.
1 parent dbd63dd commit 320a106

File tree

15 files changed

+1445
-21
lines changed

15 files changed

+1445
-21
lines changed

.changeset/add-when-failed-option.md

Lines changed: 0 additions & 6 deletions
This file was deleted.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
'@pgflow/core': minor
3+
'@pgflow/dsl': minor
4+
---
5+
6+
Add conditional step execution with skip infrastructure
7+
8+
**New DSL Options:**
9+
10+
- `if` - Run step only when input contains specified pattern
11+
- `ifNot` - Run step only when input does NOT contain pattern
12+
- `whenUnmet` - Control behavior when condition not met (fail/skip/skip-cascade)
13+
- `retriesExhausted` - Control behavior after all retries fail (fail/skip/skip-cascade)
14+
15+
**New Types:**
16+
17+
- `ContainmentPattern<T>` - Type-safe JSON containment patterns for conditions
18+
- `StepMeta` - Track skippable dependencies for proper type inference
19+
20+
**Schema Changes:**
21+
22+
- New columns: required_input_pattern, forbidden_input_pattern, when_unmet, when_failed, skip_reason, skipped_at
23+
- New step status: 'skipped'
24+
- New function: cascade_skip_steps() for skip propagation
25+
- FlowShape condition fields for auto-compilation drift detection

.changeset/skip-infrastructure-schema.md

Lines changed: 0 additions & 5 deletions
This file was deleted.

pkgs/website/astro.config.mjs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,10 @@ export default defineConfig({
253253
label: 'Retrying steps',
254254
link: '/build/retrying-steps/',
255255
},
256+
{
257+
label: 'Graceful Failure',
258+
link: '/build/graceful-failure/',
259+
},
256260
{
257261
label: 'Validation steps',
258262
link: '/build/validation-steps/',
@@ -271,6 +275,10 @@ export default defineConfig({
271275
},
272276
],
273277
},
278+
{
279+
label: 'Conditional Steps',
280+
autogenerate: { directory: 'build/conditional-steps/' },
281+
},
274282
{
275283
label: 'Starting Flows',
276284
autogenerate: { directory: 'build/starting-flows/' },
79.8 KB
Loading

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: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
---
2+
title: Examples
3+
description: AI/LLM workflow patterns using conditional execution.
4+
sidebar:
5+
order: 4
6+
---
7+
8+
import { Aside } from '@astrojs/starlight/components';
9+
10+
This page shows AI/LLM workflow patterns that benefit from conditional execution. Each example includes a diagram and condensed flow code.
11+
12+
<Aside type="tip" title="Handler Syntax">
13+
These examples use condensed syntax: root steps receive `flowInput`, dependent
14+
steps receive `deps`, and `ctx.flowInput` provides flow input in dependent
15+
steps.
16+
</Aside>
17+
18+
## Query Routing
19+
20+
Route to different handlers based on input. Simple questions go to a fast model, complex reasoning to a powerful model, and code questions to a code-specialized model.
21+
22+
```d2 width="700" pad="20"
23+
...@../../../../assets/pgflow-theme.d2
24+
25+
direction: right
26+
27+
input: "Query" { class: neutral }
28+
classify: "Classify" { class: step_completed }
29+
simple: "Simple" { class: step_skipped }
30+
complex: "Complex" { class: step_skipped }
31+
code: "Code" { class: step_started }
32+
respond: "Respond" { class: step_created }
33+
34+
input -> classify
35+
classify -> simple { style.stroke-dash: 3 }
36+
classify -> complex { style.stroke-dash: 3 }
37+
classify -> code: "intent=code"
38+
simple -> respond { style.stroke-dash: 3 }
39+
complex -> respond { style.stroke-dash: 3 }
40+
code -> respond
41+
```
42+
43+
```typescript {7,15,23}
44+
new Flow<{ query: string }>({ slug: 'query_router' })
45+
.step({ slug: 'classify' }, (flowInput) => classifyIntent(flowInput.query))
46+
.step(
47+
{
48+
slug: 'simple',
49+
dependsOn: ['classify'],
50+
if: { classify: { intent: 'simple' } },
51+
},
52+
async (_, ctx) => callFastModel((await ctx.flowInput).query)
53+
)
54+
.step(
55+
{
56+
slug: 'complex',
57+
dependsOn: ['classify'],
58+
if: { classify: { intent: 'complex' } },
59+
},
60+
async (_, ctx) => callReasoningModel((await ctx.flowInput).query)
61+
)
62+
.step(
63+
{
64+
slug: 'code',
65+
dependsOn: ['classify'],
66+
if: { classify: { intent: 'code' } },
67+
},
68+
async (_, ctx) => callCodeModel((await ctx.flowInput).query)
69+
)
70+
.step(
71+
{
72+
slug: 'respond',
73+
dependsOn: ['simple', 'complex', 'code'],
74+
},
75+
(deps) => format(deps.simple ?? deps.complex ?? deps.code)
76+
);
77+
```
78+
79+
**Key points:**
80+
81+
- Intent classification determines which model handles the query
82+
- Only ONE model runs per query - others are skipped
83+
- `respond` uses `??` to coalesce the single defined output
84+
85+
---
86+
87+
## Conditional Fallback
88+
89+
Enrich only when the primary source is insufficient. If retrieval returns low-confidence results, fall back to web search for current information.
90+
91+
```d2 width="600" pad="20"
92+
...@../../../../assets/pgflow-theme.d2
93+
94+
direction: right
95+
96+
query: "Query" { class: neutral }
97+
retrieve: "Retrieve" { class: step_completed }
98+
web: "Web Search" { class: step_started }
99+
generate: "Generate" { class: step_created }
100+
101+
query -> retrieve
102+
retrieve -> web: "low confidence"
103+
retrieve -> generate
104+
web -> generate
105+
```
106+
107+
```typescript {7}
108+
new Flow<{ query: string }>({ slug: 'rag_fallback' })
109+
.step({ slug: 'retrieve' }, (flowInput) => vectorSearch(flowInput.query)) // embedding happens inside
110+
.step(
111+
{
112+
slug: 'web',
113+
dependsOn: ['retrieve'],
114+
if: { retrieve: { confidence: 'low' } },
115+
retriesExhausted: 'skip', // Continue if web search fails
116+
},
117+
async (_, ctx) => searchWeb((await ctx.flowInput).query)
118+
)
119+
.step(
120+
{
121+
slug: 'generate',
122+
dependsOn: ['retrieve', 'web'],
123+
},
124+
async (deps, ctx) => {
125+
const docs = [...deps.retrieve.docs, ...(deps.web ?? [])];
126+
return generateAnswer((await ctx.flowInput).query, docs);
127+
}
128+
);
129+
```
130+
131+
<Aside type="note">
132+
Web search only runs when retrieval confidence is low. This saves API costs
133+
and latency for queries the knowledge base can answer well.
134+
</Aside>
135+
136+
**Key points:**
137+
138+
- Retrieval always runs first to check knowledge base
139+
- Web search is conditional on low confidence scores
140+
- `retriesExhausted: 'skip'` ensures graceful degradation if web search fails
141+
142+
---
143+
144+
## Graceful Failure Handling
145+
146+
Continue execution when steps fail. Search multiple sources in parallel - if any source fails, continue with the others.
147+
148+
```d2 width="700" pad="20"
149+
...@../../../../assets/pgflow-theme.d2
150+
151+
direction: right
152+
153+
query: "Query" { class: neutral }
154+
embed: "Embed" { class: step_completed }
155+
vector: "Vector" { class: step_completed }
156+
keyword: "Keyword" { class: step_completed }
157+
graph: "Graph" { class: step_skipped }
158+
rerank: "Rerank" { class: step_started }
159+
160+
query -> embed
161+
embed -> vector
162+
embed -> keyword
163+
embed -> graph { style.stroke-dash: 3 }
164+
vector -> rerank
165+
keyword -> rerank
166+
graph -> rerank { style.stroke-dash: 3 }
167+
```
168+
169+
```typescript {7,15,23}
170+
new Flow<{ query: string }>({ slug: 'multi_retrieval' })
171+
.step({ slug: 'embed' }, (flowInput) => createEmbedding(flowInput.query))
172+
.step(
173+
{
174+
slug: 'vector',
175+
dependsOn: ['embed'],
176+
retriesExhausted: 'skip',
177+
},
178+
(deps) => searchPinecone(deps.embed.vector)
179+
)
180+
.step(
181+
{
182+
slug: 'keyword',
183+
dependsOn: ['embed'],
184+
retriesExhausted: 'skip',
185+
},
186+
async (_, ctx) => searchElastic((await ctx.flowInput).query)
187+
)
188+
.step(
189+
{
190+
slug: 'graph',
191+
dependsOn: ['embed'],
192+
retriesExhausted: 'skip',
193+
},
194+
async (_, ctx) => searchNeo4j((await ctx.flowInput).query)
195+
)
196+
.step(
197+
{
198+
slug: 'rerank',
199+
dependsOn: ['vector', 'keyword', 'graph'],
200+
},
201+
async (deps, ctx) => {
202+
const all = [
203+
...(deps.vector ?? []),
204+
...(deps.keyword ?? []),
205+
...(deps.graph ?? []),
206+
];
207+
return rerankResults((await ctx.flowInput).query, all);
208+
}
209+
);
210+
```
211+
212+
**Key points:**
213+
214+
- Three retrieval sources run **in parallel** after embedding
215+
- Each source has `retriesExhausted: 'skip'` for resilience
216+
- `rerank` combines available results - handles undefined sources gracefully
217+
218+
---
219+
220+
## Layered Conditions
221+
222+
Combine `skip` and `skip-cascade` for nested conditionals. If tool use is needed, validate with guardrails before execution. Skip the entire tool branch if no tool is needed.
223+
224+
```d2 width="650" pad="20"
225+
...@../../../../assets/pgflow-theme.d2
226+
227+
direction: right
228+
229+
input: "Message" { class: neutral }
230+
plan: "Plan" { class: step_completed }
231+
validate: "Guardrails" { class: step_completed }
232+
execute: "Execute" { class: step_started }
233+
respond: "Respond" { class: step_created }
234+
235+
input -> plan
236+
plan -> validate: "needsTool"
237+
plan -> respond
238+
validate -> execute: "approved"
239+
validate -> respond { style.stroke-dash: 3 }
240+
execute -> respond
241+
```
242+
243+
```typescript {7-8,16-17}
244+
new Flow<{ message: string }>({ slug: 'agent_guardrails' })
245+
.step({ slug: 'plan' }, (flowInput) => planAction(flowInput.message))
246+
.step(
247+
{
248+
slug: 'validate',
249+
dependsOn: ['plan'],
250+
if: { plan: { needsTool: true } },
251+
whenUnmet: 'skip-cascade', // No tool needed = skip validation AND execution
252+
},
253+
(deps) => validateWithGuardrails(deps.plan.toolName, deps.plan.toolArgs)
254+
)
255+
.step(
256+
{
257+
slug: 'execute',
258+
dependsOn: ['plan', 'validate'],
259+
if: { validate: { approved: true } },
260+
whenUnmet: 'skip', // Rejected = skip execution, still respond
261+
},
262+
(deps) => executeTool(deps.plan.toolName!, deps.plan.toolArgs!)
263+
)
264+
.step(
265+
{
266+
slug: 'respond',
267+
dependsOn: ['plan', 'execute'],
268+
},
269+
async (deps, ctx) =>
270+
generateResponse((await ctx.flowInput).message, deps.execute)
271+
);
272+
```
273+
274+
<Aside type="caution">
275+
Note the different skip modes: `validate` uses `skip-cascade` (no tool needed
276+
= skip everything downstream), while `execute` uses `skip` (rejected by
277+
guardrails = skip execution but still respond).
278+
</Aside>
279+
280+
**Key points:**
281+
282+
- `skip-cascade` on validation skips the entire tool branch when no tool is needed
283+
- `skip` on execution allows responding even when guardrails reject
284+
- Layered conditions: tool needed → guardrails approved → execute
285+
286+
---
287+
288+
## Pattern Comparison
289+
290+
| Pattern | Use Case | Skip Mode | Output Type |
291+
| -------------------- | --------------------------- | -------------- | ------------------------ |
292+
| Query Routing | Mutually exclusive branches | `skip` | `T` or `undefined` |
293+
| Conditional Fallback | Enrich only when needed | `skip` | `T` or `undefined` |
294+
| Graceful Failure | Continue when steps fail | `skip` | `T` or `undefined` |
295+
| Layered Conditions | Nested skip + skip-cascade | `skip-cascade` | `T` (guaranteed if runs) |
296+
297+
<Aside type="tip">
298+
Use `skip` when downstream steps should handle missing data. Use
299+
`skip-cascade` when an entire branch should be skipped together.
300+
</Aside>

0 commit comments

Comments
 (0)