Skip to content

Commit 208f76f

Browse files
committed
add docs for conditional steps
1 parent 963cad9 commit 208f76f

15 files changed

Lines changed: 1442 additions & 21 deletions

.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)