04: Workflow
Not everything needs an agent.
If the steps are known ahead of time, Python should usually control the path. For a simple travel assistant, the path might be:
extract preferences -> draft itinerary -> format answer
Do not ask the model to freely choose the next step when the code already knows it. Fixed flow is cheaper and easier to test.
Code First
from __future__ import annotations
from pathlib import Path
from agent_patterns_lab.patterns.workflow_chaining import PromptStep, run_prompt_chain
from agent_patterns_lab.runtime import MockLLM, Tracer
def main() -> None:
tracer = Tracer()
model = MockLLM(
[
"Preferences: tea, local food, easy walking. Constraint: relaxed one-day trip.",
"Draft: Morning West Lake, afternoon Tea Museum, evening Hefang Street.",
"Final itinerary: West Lake -> China National Tea Museum -> Hefang Street.",
]
)
steps = [
PromptStep(name="extract_preferences", user_prompt="Extract travel preferences from: {input}"),
PromptStep(name="draft_itinerary", user_prompt="Draft an itinerary from these preferences:\n{input}"),
PromptStep(name="format_final", user_prompt="Format this itinerary as a concise final answer:\n{input}"),
]
out = run_prompt_chain(
model,
initial_input="Plan a relaxed one-day Hangzhou trip. I like tea, local food, and easy walking.",
steps=steps,
tracer=tracer,
)
print(out)
trace_path = tracer.export_jsonl(Path(".traces") / "11_prompt_chaining.jsonl")
print(f"[trace] {trace_path}")
if __name__ == "__main__":
main()
Run:
uv run python examples/11_prompt_chaining.py
Expected output:
Final itinerary: West Lake -> China National Tea Museum -> Hefang Street.
[trace] .traces/11_prompt_chaining.jsonl
What Changed
The task is split into three PromptSteps:
steps = [
PromptStep(name="extract_preferences", user_prompt="Extract travel preferences from: {input}"),
PromptStep(name="draft_itinerary", user_prompt="Draft an itinerary from these preferences:\n{input}"),
PromptStep(name="format_final", user_prompt="Format this itinerary as a concise final answer:\n{input}"),
]
Then they run in order:
out = run_prompt_chain(
model,
initial_input="Plan a relaxed one-day Hangzhou trip. I like tea, local food, and easy walking.",
steps=steps,
tracer=tracer,
)
This is Prompt Chaining: each step's output becomes the next step's input.
flowchart LR
I["User request"] --> A["Extract preferences"]
A --> B["Draft itinerary"]
B --> C["Format answer"]
C --> O["Final itinerary"]
What Workflow Fixes
The point of a workflow is that code owns the path.
| Shape | Who decides next |
|---|---|
| One-shot chatbot | No next step |
| Workflow | Python predefines the next step |
| Agent Loop | Model chooses from current state |
If the flow is fixed, workflows are good:
- The trace shows exactly which step broke.
- Each step can be tested.
- Cost and latency are easier to control.
- The model will not randomly pick an unrelated tool.
What It Does Not Fix
Workflow assumes you already know the steps.
Real travel planning often breaks that assumption:
- The weather says afternoon rain. Should the next step search indoor places?
- A place is closed. Should the assistant search again?
- The budget is too low. Should it remove a paid attraction?
- The route is too long. Should it estimate a new order?
If you encode all of that into a workflow, the code turns into a large if-else tree.
When the next step truly depends on a fresh observation, use an agent loop.
Next: 05: Agent Loop.