Skip to content

Commit 56ec88e

Browse files
committed
add docs for conditional steps
1 parent 963cad9 commit 56ec88e

File tree

10 files changed

+1632
-10
lines changed

10 files changed

+1632
-10
lines changed

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/' },

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

0 commit comments

Comments
 (0)