Flujos de Aprobación
Implementa flujos de aprobación estructurados con interrupciones condicionales, interrupciones antes/después de nodos y enrutamiento dinámico humano-en-el-bucle.
Flujos de Aprobación
Approval workflows combine interrupts with conditional routing to create structured human-in-the-loop processes. This lesson covers before-node interrupts, after-node interrupts, and dynamic approval patterns.
Visión General del Patrón de Aprobación
Agent proposes action → Human reviews → Approve? → Yes → Execute
↓ No
Reject → Provide feedback → Revise or Stop
The agent presents a proposal, the human decides, and execution follows accordingly.
Interrupción Antes del Nodo
Interrupt before a node executes — the human decides whether the node should run:
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import MemorySaver
from langgraph.types import interrupt, Command
from typing_extensions import TypedDict
class ApprovalState(TypedDict):
query: str
search_plan: str
approved: bool
feedback: str
result: str
def plan_node(state: ApprovalState) -> dict:
plan = f"Search for: {state['query']}"
return {"search_plan": plan}
def approve_search(state: ApprovalState) -> dict:
# BEFORE interrupt — human decides if search should happen
response = interrupt({
"type": "approval",
"agent_plan": state["search_plan"],
"prompt": "Approve this search operation?"
})
return {"approved": response.get("approved", False),
"feedback": response.get("feedback")}
def execute_search(state: ApprovalState) -> dict:
if not state["approved"]:
return {"result": f"Rejected. Feedback: {state.get('feedback')}"}
return {"result": f"Executed: {state['search_plan']}"}
def after_search_review(state: ApprovalState) -> dict:
# AFTER interrupt — human reviews results
response = interrupt({
"type": "review",
"result": state["result"],
"prompt": "Are these results acceptable?"
})
return {"approved": response.get("approved", False)}
builder = StateGraph(ApprovalState)
builder.add_node("plan", plan_node)
builder.add_node("approve_search", approve_search)
builder.add_node("execute", execute_search)
builder.add_edge(START, "plan")
builder.add_edge("plan", "approve_search")
builder.add_edge("approve_search", "execute")
builder.add_edge("execute", END)
app = builder.compile(checkpointer=MemorySaver())[!NOTA] A before-node interrupt prevents an action from happening until approved. This is the most common approval pattern — it prevents unauthorized or risky actions.
Interrupción Después del Nodo
Interrupt after a node executes — the human reviews the result:
def draft_email(state: EmailState) -> dict:
email = llm.invoke(f"Draft email about: {state['topic']}")
return {"draft": email.content}
def review_draft(state: EmailState) -> dict:
# AFTER interrupt — human reviews the draft
response = interrupt({
"type": "review",
"draft": state["draft"],
"prompt": "Edit or approve this email draft?"
})
return {
"approved": response.get("approved", False),
"revision": response.get("revision", "")
}
def finalize_or_revise(state: EmailState) -> dict:
if state["approved"]:
return {"final": state["draft"]}
return {"draft": state["revision"], "approved": False}[!CONSEJO] After-node interrupts are useful for content review, result validation, and any workflow where the human should verify output before it's used.
Enrutamiento Condicional de Aprobación
Route based on whether the human approved or rejected:
def approval_router(state: ApprovalState) -> str:
if state["approved"]:
return "approved"
return "rejected"
builder.add_conditional_edges(
"human_review",
approval_router,
{
"approved": "execute",
"rejected": "revise_plan" # Send back for revision
}
)Patrón de Bucle de Revisión
class RevisionState(TypedDict):
draft: str
approved: bool
revision_count: int
def generate_draft(state: RevisionState) -> dict:
draft = llm.invoke(f"Write content about: {state['topic']}")
return {"draft": draft.content}
def human_review(state: RevisionState) -> dict:
response = interrupt({
"draft": state["draft"],
"revision_count": state["revision_count"],
"prompt": "Approve, or provide revision instructions."
})
return {
"approved": response.get("approved", False),
"feedback": response.get("feedback", ""),
"revision_count": state["revision_count"] + 1
}
def revise_draft(state: RevisionState) -> dict:
revised = llm.invoke(
f"Revise this draft based on feedback:\n"
f"Draft: {state['draft']}\nFeedback: {state['feedback']}"
)
return {"draft": revised.content}
def revision_router(state: RevisionState) -> str:
if state["approved"]:
return "finalize"
if state["revision_count"] >= 5:
return "max_revisions"
return "revise"
builder.add_conditional_edges(
"human_review",
revision_router,
{
"finalize": "finalize",
"revise": "revise_draft",
"max_revisions": "error_handler"
}
)
builder.add_edge("revise_draft", "human_review") # Loop back for another review[!ADVERTENCIA] Always cap revision loops. Without a maximum, an unsatisfied reviewer could loop forever.
Flujo de Escalada
Route to a senior reviewer if junior rejects or if the request is high-risk:
class EscalationState(TypedDict):
request: str
risk_level: str
junior_approved: bool
senior_approved: bool
def junior_review(state: EscalationState) -> dict:
response = interrupt({"request": state["request"], "risk": state["risk_level"]})
return {"junior_approved": response.get("approved", False)}
def escalate(state: EscalationState) -> dict:
response = interrupt({"request": state["request"],
"risk": state["risk_level"],
"note": "Escalated from junior review"})
return {"senior_approved": response.get("approved", False)}
def escalation_router(state: EscalationState) -> str:
if state["junior_approved"]:
return "approved"
if state["risk_level"] == "high":
return "escalate"
return "rejected"
builder.add_conditional_edges(
"junior_review",
escalation_router,
{
"approved": "execute",
"escalate": "senior_review",
"rejected": "reject_node"
}
)Aprobación con Datos Dinámicos
Include relevant data in the approval request:
def approval_with_context(state: State) -> dict:
# Gather context for the human reviewer
context = {
"request_id": state["request_id"],
"requester": state["user_name"],
"action": state["proposed_action"],
"cost_estimate": state.get("estimated_cost", "Unknown"),
"similar_approved_count": state.get("similar_count", 0),
"policy_reference": "Policy #42: External API calls require approval",
"timestamp": datetime.now().isoformat()
}
response = interrupt(context)
return {"approval_response": response}Timeout para Aprobaciones
Handle the case where the human never responds:
def approval_with_timeout(state: State) -> dict:
# Set a timeout via configuration
config = state.get("config", {})
timeout = config.get("approval_timeout_minutes", 60)
response = interrupt({
"prompt": "Approve within {timeout} minutes or it will be auto-rejected.",
"request": state["request"]
})
# If human doesn't respond within timeout,
# calling invoke again with a timeout command handles it
return {"human_response": response}[!NOTA] LangGraph does not have built-in timeout for interrupts. Implement timeout logic at the application layer by checking how long a thread has been paused.
Ejemplo Completo de Flujo de Aprobación
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import MemorySaver
from langgraph.types import interrupt, Command
from typing_extensions import TypedDict
from datetime import datetime
class WorkflowState(TypedDict):
request: str
risk_score: float
draft_response: str
approved: bool
feedback: str
result: str
step: str
def analyze_request(state: WorkflowState) -> dict:
risk = len(state["request"]) * 0.01 # Simplified risk calc
return {"risk_score": min(risk, 1.0), "step": "analyzed"}
def generate_draft(state: WorkflowState) -> dict:
draft = f"Response to: {state['request']}"
return {"draft_response": draft, "step": "drafted"}
def human_approval(state: WorkflowState) -> dict:
response = interrupt({
"request": state["request"],
"draft": state["draft_response"],
"risk": state["risk_score"],
"timestamp": datetime.now().isoformat()
})
return {"approved": response.get("approved", False),
"feedback": response.get("feedback", ""),
"step": "reviewed"}
def execute(state: WorkflowState) -> dict:
if state["approved"]:
return {"result": state["draft_response"], "step": "completed"}
return {"result": f"Rejected: {state.get('feedback')}", "step": "rejected"}
builder = StateGraph(WorkflowState)
builder.add_node("analyze", analyze_request)
builder.add_node("draft", generate_draft)
builder.add_node("approve", human_approval)
builder.add_node("execute", execute)
builder.add_edge(START, "analyze")
builder.add_edge("analyze", "draft")
builder.add_edge("draft", "approve")
builder.add_edge("approve", "execute")
builder.add_edge("execute", END)
app = builder.compile(checkpointer=MemorySaver())Preguntas Prácticas
What is a before-node interrupt?
What is an after-node interrupt useful for?
How do you create a revision loop in an approval workflow?
Why should revision loops have a maximum count?
What is an escalation workflow?
How do you include context for the human reviewer in an interrupt?
What determines which path the graph takes after an approval interrupt?
What happens when a human rejects a proposal in an approval workflow?
Can an approval workflow have multiple sequential approval steps?
How does the approval workflow pattern enhance LangGraph agents?
[!ÉXITO]
Conclusiones Clave
- Before-node interrupts prevent actions until human approval
- After-node interrupts let humans review results before proceeding
- Conditional edges route based on approval/rejection decisions
- Revision loops cycle until approval or max revisions reached
- Escalation workflows route to senior reviewers when needed
- Pass rich context in interrupt() to help humans make informed decisions
- Always cap revision and escalation loops with maximum counts
- Multiple sequential approvals can be chained in a single graph