Four Approaches, Fundamentally Different
Choosing a workflow framework means matching execution model, engineering cost, and team capability. There's no universally better option.
Approach Workflow definition State persistence Execution engine
──────────────────────────────────────────────────────────────────────────────
Prompt-based Markdown + YAML Hand-written JSON LLM (A-layer)
LangGraph Python code (graph) Built-in State Python code (deterministic)
Temporal Python/TypeScript Built-in (database) Code (deterministic)
n8n Visual canvas + JSON Built-in Code (deterministic)
The first three support semantic branching (confidence >= 0.95). n8n is limited to boolean expressions. LangGraph and Temporal use deterministic code as the execution engine; Prompt-based uses the LLM itself.
Prompt-based
Workflow definition: Markdown + YAML files
# workflow.md
## Phase 3: Root Cause Analysis
Execute subagent: rnd-automotive-issue-analyzer
Context: {{ phases.phase2.log_dir }}
Routing:
- confidence >= 0.95 → Phase 4
- 0.6 <= confidence < 0.95 → Gate A
- confidence < 0.6 and retries < 3 → retry Phase 3
- confidence < 0.6 and retries >= 3 → human escalation
Strengths:
- Non-engineers can read and modify the workflow definition
- Changing Markdown is faster than changing code — good for frequent iteration
- LLM executes routing logic; not every edge case requires hardcoding
- Low startup cost; right for POC and rapid validation
Weaknesses:
- LLM-executed routing has non-determinism: same input may route differently across runs
- No language-level type system or testing tool support
- Observability requires manual Langfuse integration — not out of the box
- Maintenance difficulty grows as the number of Markdown files increases
Best for:
- Workflows that change frequently, where editing Markdown beats editing code
- Teams without dedicated engineers, or where non-technical people maintain workflow logic
- Rapid POC validation — working MVP in 3 days
LangGraph
Workflow definition: Python code (graph structure)
from langgraph.graph import StateGraph, END
from typing import TypedDict
class WorkflowState(TypedDict):
jira_key: str
bug_info: dict # Phase 1 output
analysis: dict # Phase 3 output
fix_result: dict # Phase 4 output
analyze_retries: int
def analyze_node(state: WorkflowState) -> dict:
result = call_skill("rnd-automotive-issue-analyzer", state["bug_info"])
return {
"analysis": result,
"analyze_retries": state["analyze_retries"] + 1
}
def route_after_analyze(state: WorkflowState) -> str:
confidence = state["analysis"]["confidence"]
retries = state["analyze_retries"]
if confidence >= 0.95: return "fix_and_verify"
if confidence >= 0.6: return "gate_A"
if retries < 3: return "analyze" # retry
return "human_escalation"
graph = StateGraph(WorkflowState)
graph.add_node("analyze", analyze_node)
graph.add_conditional_edges("analyze", route_after_analyze, {
"fix_and_verify": "fix_and_verify",
"analyze": "analyze",
"gate_A": "gate_A",
"human_escalation": "human_escalation",
})
LangGraph concepts mapped to Prompt-based equivalents:
LangGraph concept Prompt-based equivalent
────────────────────────────────────────────────────────
WorkflowState (TypedDict) workflow_state.json structure
Node function Each Phase's execution logic
conditional_edges Routing conditions in workflow.md
checkpointer workflow_state.json itself
interrupt (human-in-loop) Approval gate
Understanding this mapping makes conversion systematic — not a rewrite from scratch.
Strengths:
- Routing logic is pure Python; deterministic and unit-testable
- TypedDict State Schema provides type checking — missing fields caught at development time
- Built-in LangSmith integration; Trace is out-of-the-box
- Supports nested subgraphs for complex state machines
Weaknesses:
- Workflow definition is code; non-engineers can't read or modify it
- Changes go through code review — iteration is slower than Markdown
- Learning curve: requires understanding graph structure and the StateGraph API
Best for:
- Complex workflow logic requiring precise state machine control
- Teams comfortable with Python, with relatively stable (not daily-changing) workflows
- Code-level type checking and test coverage requirements
Temporal
Workflow definition: Python or TypeScript code
from temporalio import workflow, activity
from datetime import timedelta
@activity.defn
async def analyze_bug(bug_info: dict) -> dict:
return await call_skill("rnd-automotive-issue-analyzer", bug_info)
@workflow.defn
class BugFixWorkflow:
@workflow.run
async def run(self, jira_key: str) -> dict:
bug_info = await workflow.execute_activity(
fetch_jira_ticket,
jira_key,
start_to_close_timeout=timedelta(minutes=5)
)
analysis = await workflow.execute_activity(
analyze_bug,
bug_info,
start_to_close_timeout=timedelta(minutes=30),
retry_policy=RetryPolicy(maximum_attempts=3)
)
# ...
Strengths:
- True Durable Execution at the code layer — crash recovery guaranteed without hand-written state files
- Native support for long-running workflows (days, weeks), suited for SLA-governed enterprise processes
- Built-in visualization UI and complete workflow history
- Strict separation of Activities and Workflows
Weaknesses:
- Complex deployment: requires a running Temporal Server
- High learning curve: Temporal's programming model differs from standard async code in non-obvious ways
- Temporal's Activity timeout and retry model doesn't cleanly match LLM call behavior
Best for:
- Workflows running longer than an hour, involving human approval with extended wait times
- Enterprise-grade processes requiring strong consistency guarantees
- Teams with dedicated backend engineers maintaining infrastructure
n8n
Workflow definition: Visual canvas + JSON
[HTTP Request] → [AI Agent] → [Condition] → [Jira Comment]
↓
[Send Email]
Strengths:
- Visual; non-technical users can build and understand workflows
- Large library of built-in integration nodes (Jira, GitHub, Slack, databases)
- Self-hosted; data stays on-premises
Weaknesses:
- Branching is limited to boolean expressions; semantic routing requires an external LLM node
- Complex state management (retry counters, candidate result aggregation) is clumsy in the visual interface
- Version control: JSON file diffs are unreadable; merge conflicts are painful
- AI Agent nodes have limited capability — can't implement Orchestrator-Subagents patterns
Best for:
- Primarily API integration, with AI as one step among many
- Workflows that need to be visible to non-technical stakeholders
- Simple tasks with no complex state management requirements
Decision Tree
Workflow changes frequently; non-engineers need to maintain it?
→ Prompt-based (Markdown)
Complex logic; code-level tests; team knows Python?
→ LangGraph
Runtime > 1 hour; enterprise SLA; dedicated backend team?
→ Temporal
Primarily API integration; AI is one node; visual presentation needed?
→ n8n
Hybrid use: These approaches aren't mutually exclusive. A real system might use n8n for triggers and notification integrations (listen for new Jira tickets → send Feishu notification), while LangGraph or Prompt-based handles the core AI Agent workflow logic.
Migrating from Prompt-based to LangGraph
The mapping is systematic — the work is translating implicit LLM routing into explicit Python, not redesigning the workflow.
Step 1: Convert workflow.md's context structure to WorkflowState
class WfBugE2EState(TypedDict):
jira_key: str
bug_info: dict | None
log_dir: str | None
analysis: dict | None
fix_results: list[dict] # Phase 4 parallel candidate results
selected_fix: dict | None # Phase 4 fan-in result
commit_result: dict | None
analyze_retries: int
Step 2: Convert each Phase's template to a Node function
def phase3_analyze_node(state: WfBugE2EState) -> dict:
result = spawn_subagent(
template="templates/analyze.md",
context={"bug_info": state["bug_info"], "log_dir": state["log_dir"]}
)
return {
"analysis": result,
"analyze_retries": state["analyze_retries"] + 1,
}
Step 3: Convert workflow.md's routing conditions to conditional_edges
def route_after_analyze(state: WfBugE2EState) -> str:
conf = state["analysis"]["confidence"]
retries = state["analyze_retries"]
if conf >= 0.95: return "phase_4_fix"
if conf >= 0.6: return "gate_A"
if retries < 3: return "phase_3_analyze"
return "human_escalation"
The core design (Phase structure, Context passing, approval gates) is identical in both. What changes is moving routing logic from "LLM interprets the condition" to "Python evaluates the condition."
Summary
- No universally best framework — only the best fit: Prompt-based for fast iteration and non-technical maintenance; LangGraph for complex state machines with code-level tests; Temporal for long-running enterprise processes; n8n for API integration and visual presentation
- LangGraph and Prompt-based are convertible: WorkflowState = workflow_state.json, Node = Phase/Step, conditional_edges = routing conditions — one-to-one mapping, systematic migration
- Framework choice doesn't change the core engineering: Context passing modes, approval gates, idempotency, version binding — these principles apply regardless of which framework you use
Check out PrimeSkills — a curated marketplace of AI agents and skills that have been validated in real-world, enterprise-grade workflows. No fluff, just what actually works.
Find more useful knowledge and interesting products on my Homepage
Top comments (0)