Skip to content

How Do You Handle Conditional Routing and Branching in File-Based AI Agent Workflows?

I had a file-based AI agent that worked beautifully for storing context. It remembered every conversation, tracked every task, and never forgot a user preference. But when I tried to make it branch based on its output, everything fell apart.

The agent would finish a research task and just… stop. It didn’t know whether to continue to writing, or to ask for clarification, or to escalate to a human. The markdown files held all the information, but no logic to act on it.

I posted about this problem and got a blunt reality check:

“Routing logic is where it falls apart. You still need code for conditionals, error handling, retries, rate limiting. The best setups I’ve seen use both. File-based memory, code-based orchestration.” - Dependent_Slide4675

They were right. Markdown files are great for storing knowledge, but they can’t evaluate conditions or make decisions. Let me show you how I solved this.

The Problem: Files Can’t Think

My file-based agent had this structure:

Directory Structure
agents/
research-agent/
state.md # Current state
memory/
facts.md # What it knows
preferences.md # User preferences
tasks/
active.md # Current task
completed/ # Done tasks

The state.md file looked like this:

state.md
# Agent State
## Current Task
Research competitor pricing for SaaS tools
## Status
Research Complete
## Last Action
Called search_web tool with query: "SaaS pricing comparison 2024"
## Findings
- Found 15 relevant articles
- Extracted pricing for 8 competitors
- Need to analyze pricing tiers

Everything is there. But how does the agent know what to do next?

I tried encoding logic in the markdown:

state.md (failed attempt)
# Agent State
## Status
Research Complete
## Routing Rules
- IF status == "Research Complete" AND findings > 10: GOTO analyze
- IF status == "Research Complete" AND findings < 5: GOTO clarify
- IF status == "Analysis Complete": GOTO report

This doesn’t work. Markdown is for humans to read, not for machines to execute. The agent would read these “rules” and then ignore them because it had no way to evaluate them.

What File-Based Systems Can and Cannot Do

Let me be clear about the limitations:

File-based capabilities
CAN DO:
+ Store context and memory
+ Hold skill definitions
+ Provide human-readable instructions
+ Version control agent state
+ Debug by reading files
CANNOT DO:
- Conditional logic (if/then/else)
- Looping mechanisms
- State evaluation and branching
- Error handling and retries
- Rate limiting

I was trying to make markdown do something it was never designed for. The solution wasn’t to add more rules to files—it was to add code that reads those files.

The Solution: Hybrid Architecture

I restructured my agent to use markdown for knowledge and code for decisions:

Hybrid architecture
+------------------------------------------+
| Code Orchestration |
| +------------+------------+------------+|
| | Router | Retrier |Rate Limiter||
| +------------+------------+------------+|
| | |
+------------------------------------------+
|
+------------------------------------------+
| File-Based Layer |
| +------------+------------+------------+|
| | Skills | Memory | Context ||
| | (.md) | (.md) | (.md) ||
| +------------+------------+------------+|
+------------------------------------------+

The code layer handles all the conditional logic. The file layer stores all the knowledge. They work together.

Implementing Conditional Routing with LangGraph

I chose LangGraph for the orchestration layer. It provides a clean way to define states and transitions:

routing.py
from langgraph.graph import StateGraph, END
from typing import TypedDict, Literal
from pathlib import Path
class AgentState(TypedDict):
task: str
status: str
findings_count: int
next_action: str
retry_count: int
max_retries: int
def route_decision(state: AgentState) -> Literal["analyze", "clarify", "report", "end"]:
"""Conditional router based on state evaluation."""
# Check retry limit first
if state["retry_count"] >= state["max_retries"]:
return "end"
# Route based on findings
if state["status"] == "research_complete":
if state["findings_count"] >= 10:
return "analyze"
elif state["findings_count"] >= 5:
return "clarify"
else:
return "end" # Not enough data
if state["status"] == "analysis_complete":
return "report"
if state["status"] == "report_complete":
return "end"
return "end"
# Build the workflow graph
workflow = StateGraph(AgentState)
workflow.add_node("research", research_node)
workflow.add_node("analyze", analyze_node)
workflow.add_node("clarify", clarify_node)
workflow.add_node("report", report_node)
# Add conditional edges
workflow.add_conditional_edges(
"research",
route_decision,
{
"analyze": "analyze",
"clarify": "clarify",
"report": "report",
"end": END
}
)
app = workflow.compile()

Now the agent can make decisions. But where does the state come from? It comes from the markdown files.

Reading Routing Hints from Skill Files

Here’s the hybrid part. I store routing hints in the skill markdown files:

skills/web-search.md
# Web Search Skill
## Purpose
Search the web for information.
## Expected Output
- results_found: boolean
- result_count: number
- needs_refinement: boolean
## Routing Hints
- If needs_refinement is true, route back to query refinement
- If result_count is 0, route to alternative search
- If result_count > 10, route to analysis

The code reads these hints and applies them:

hybrid_router.py
from pathlib import Path
import re
def load_skill_routing_hints(skill_name: str) -> dict:
"""Extract routing hints from skill markdown."""
skill_path = Path(f"skills/{skill_name}.md")
content = skill_path.read_text()
hints = {}
hint_section = re.search(r'## Routing Hints\n(.*?)(?=\n##|$)', content, re.DOTALL)
if hint_section:
for line in hint_section.group(1).strip().split('\n'):
if line.startswith('- '):
hints[len(hints)] = line[2:]
return hints
def route_from_skill_output(skill_name: str, output: dict) -> str:
"""Route based on skill output and defined hints."""
hints = load_skill_routing_hints(skill_name)
# Apply the logic described in hints
if output.get("needs_refinement"):
return "refine_query"
if output.get("result_count", 0) == 0:
return "alternative_search"
if output.get("result_count", 0) > 10:
return "analysis"
return "proceed"

This gives me the best of both worlds. The routing logic is documented in human-readable markdown, but executed by reliable code.

Adding Error Handling and Retries

One thing markdown absolutely cannot do is error handling. I added a retry mechanism in the orchestration layer:

retry_router.py
from dataclasses import dataclass
from typing import Callable
@dataclass
class RoutingDecision:
next_step: str
should_retry: bool = False
error_message: str = ""
class AgentRouter:
def __init__(self, max_retries: int = 3):
self.max_retries = max_retries
self.retry_counts: dict[str, int] = {}
def route_with_retry(
self,
step_name: str,
output: dict,
condition_fn: Callable[[dict], bool]
) -> RoutingDecision:
"""Evaluate output and determine routing with retry support."""
if condition_fn(output):
# Success - reset retry count and move forward
self.retry_counts[step_name] = 0
return RoutingDecision(next_step="next")
# Failure - check retry limit
current_retries = self.retry_counts.get(step_name, 0)
if current_retries < self.max_retries:
self.retry_counts[step_name] = current_retries + 1
return RoutingDecision(
next_step=step_name,
should_retry=True,
error_message=f"Retry {current_retries + 1}/{self.max_retries}"
)
# Max retries exceeded - route to error handler
return RoutingDecision(
next_step="error_handler",
error_message="Max retries exceeded"
)
# Usage
router = AgentRouter(max_retries=3)
def is_successful(output: dict) -> bool:
return output.get("status") == "success"
decision = router.route_with_retry("web_search", output, is_successful)
if decision.should_retry:
print(f"Retrying: {decision.error_message}")
else:
print(f"Next step: {decision.next_step}")

Now when the web search fails, the agent automatically retries before giving up. No markdown file could do this.

A Complete Workflow Example

Let me show you how all the pieces fit together:

complete_agent.py
from langgraph.graph import StateGraph, END
from typing import TypedDict, Literal
from pathlib import Path
class WorkflowState(TypedDict):
task: str
current_step: str
research_results: dict
analysis_results: dict
retry_count: int
class HybridAgent:
def __init__(self, agent_dir: str):
self.base_path = Path(agent_dir)
self.router = AgentRouter(max_retries=3)
self._setup_workflow()
def _setup_workflow(self):
"""Build the LangGraph workflow."""
workflow = StateGraph(WorkflowState)
# Add nodes
workflow.add_node("load_context", self._load_context)
workflow.add_node("research", self._research)
workflow.add_node("analyze", self._analyze)
workflow.add_node("report", self._report)
# Add conditional edges
workflow.add_conditional_edges(
"research",
self._route_after_research,
{"analyze": "analyze", "research": "research", "end": END}
)
workflow.add_conditional_edges(
"analyze",
self._route_after_analyze,
{"report": "report", "end": END}
)
workflow.set_entry_point("load_context")
workflow.add_edge("load_context", "research")
workflow.add_edge("report", END)
self.app = workflow.compile()
def _load_context(self, state: WorkflowState) -> WorkflowState:
"""Load context from markdown files."""
state_file = self.base_path / "state.md"
if state_file.exists():
# Parse markdown and add to state
content = state_file.read_text()
state["context"] = self._parse_markdown(content)
return state
def _route_after_research(self, state: WorkflowState) -> str:
"""Decide what to do after research."""
results = state.get("research_results", {})
# Apply routing logic
if results.get("findings_count", 0) >= 5:
return "analyze"
elif state["retry_count"] < 3:
state["retry_count"] += 1
return "research" # Try again with different approach
else:
return "end" # Give up
def _route_after_analyze(self, state: WorkflowState) -> str:
"""Decide what to do after analysis."""
if state.get("analysis_results"):
return "report"
return "end"
def run(self, task: str) -> dict:
"""Execute the workflow."""
initial_state = {
"task": task,
"current_step": "start",
"research_results": {},
"analysis_results": {},
"retry_count": 0
}
return self.app.invoke(initial_state)

This agent reads from markdown files for context, but uses code for all the decision-making.

Why This Matters

After implementing the hybrid approach, I noticed three key improvements:

Before vs After comparison
Aspect | Pure File-Based | Hybrid
--------------------|-----------------|--------
Decision making | None | Deterministic
Error handling | Manual | Automatic
Retry logic | Impossible | Configurable
Debugging | Read files | Read files + logs
State visibility | Excellent | Excellent

The hybrid approach keeps what makes file-based agents great (visibility, version control, human-readability) while adding what they lack (logic, error handling, control flow).

Common Mistakes

Mistake 1: Trying to encode logic in markdown

WRONG approach
## Conditional Logic
IF status == "complete": next = "report"
IF status == "failed": next = "retry"

This is not executable. The agent cannot evaluate these conditions.

Mistake 2: Putting everything in code

If you move all the context to code, you lose the visibility benefit. The hybrid approach keeps knowledge in files and logic in code.

Mistake 3: Over-engineering the router

Keep routing simple. If you need complex decision trees, that’s a sign you need a dedicated tool, not more routing logic.

Mistake 4: Ignoring state management

Without proper state tracking, routing becomes chaotic. Always use a state machine library like LangGraph or XState.

Summary

I spent weeks trying to make my file-based agent make decisions. The answer was simple: files can’t think, but code can.

The hybrid architecture works like this:

  1. Markdown files store context, memory, skills, and routing hints
  2. Code orchestration handles conditionals, error handling, retries, and state transitions
  3. LangGraph provides the state machine for deterministic routing

The result is an agent that has both the visibility of file-based systems and the decision-making capability of code-based systems.

If you’re building a file-based agent, don’t try to make markdown do logic. Add a code layer that reads your files and makes decisions based on them. Your future self will thank you when you can debug both the state (by reading files) and the flow (by reading code).

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments