Guide
How to add a validation node to a LangGraph agent (pass, retry, fail)
20 October 2026
How to add a validation node to a LangGraph agent (pass, retry, fail)
Short answer: Put a judge node after your worker node. Have it call a validator, store the verdict in state, and use add_conditional_edges to route: pass goes to accept, retry loops back to the worker (with a cap), and fail goes to reject. Screwtape.ai's POST /v1/judge returns exactly those three verdicts, plus a 0 to 1 score and reasons you can feed back to the worker on a retry.
Screwtape.ai is an agent validation API. It is not the arXiv paper that is also called Screwtape.
The graph
START -> worker -> judge --pass--> accept -> END
|---retry--> worker (attempts < 3)
'---fail/limit--> reject -> ENDWorking example
Tested on 2026-10-20 with langgraph 1.2.16:
- With a stubbed judge, the sequence retry, retry, pass ends
acceptedafter 3 attempts. - Retry on every attempt ends
rejectedat the cap. - Fail ends
rejectedafter 1 attempt. - With the real HTTP call and a placeholder key, the live API returned
401, which confirms the request shape.
"""LangGraph: a worker node, a Screwtape judge node, and routing on pass / retry / fail."""
import json
import os
import urllib.request
from typing import TypedDict
from langgraph.graph import END, START, StateGraph
API = "https://screwtape.ai"
MAX_ATTEMPTS = 3
def screwtape_post(path: str, payload: dict) -> dict:
"""POST JSON to Screwtape.ai. Reads SCREWTAPE_API_KEY from the environment."""
request = urllib.request.Request(
API + path,
data=json.dumps(payload).encode(),
headers={
"Authorization": "Bearer " + os.environ["SCREWTAPE_API_KEY"],
"Content-Type": "application/json",
},
method="POST",
)
with urllib.request.urlopen(request, timeout=60) as response:
return json.load(response)
class State(TypedDict, total=False):
task: str
evidence: list[str]
criteria: list[str]
agent_output: str
attempts: int
feedback: list[str]
verdict: dict
status: str
def worker(state: State) -> dict:
# Replace with your LLM call. Pass state.get("feedback") back into the prompt on a retry.
return {
"agent_output": "The crew size was 4. The mission launched from Cape Canaveral.",
"attempts": state.get("attempts", 0) + 1,
}
def judge(state: State) -> dict:
payload = {"task": state["task"], "agent_output": state["agent_output"]}
if state.get("evidence"):
payload["evidence"] = state["evidence"]
if state.get("criteria"):
payload["criteria"] = state["criteria"]
verdict = screwtape_post("/v1/judge", payload)
return {"verdict": verdict, "feedback": verdict["reasons"]}
def route(state: State) -> str:
v = state["verdict"]["verdict"]
if v == "pass":
return "accept"
if v == "retry" and state.get("attempts", 0) < MAX_ATTEMPTS:
return "worker"
return "reject"
def accept(state: State) -> dict:
return {"status": "accepted"}
def reject(state: State) -> dict:
return {"status": "rejected"}
graph = StateGraph(State)
graph.add_node("worker", worker)
graph.add_node("judge", judge)
graph.add_node("accept", accept)
graph.add_node("reject", reject)
graph.add_edge(START, "worker")
graph.add_edge("worker", "judge")
graph.add_conditional_edges("judge", route, ["accept", "worker", "reject"])
graph.add_edge("accept", END)
graph.add_edge("reject", END)
app = graph.compile()
if __name__ == "__main__":
result = app.invoke({
"task": "State the crew size and launch site.",
"evidence": [
"The mission launched on 2024-03-01 from Cape Canaveral.",
"The crew size was 4.",
],
"criteria": ["crew size", "Cape Canaveral"],
})
print(result["status"], result["verdict"])Notes:
add_conditional_edges(source, path, path_map)is the LangGraph signature. Passing a list aspath_maptells LangGraph which nodes the router can return (LangGraph Graph API docs).feedbackholds the judge'sreasons. Pass it to your model on the next attempt so the retry is informed rather than random.- Every judge call counts against your quota: 100 calls per UTC day on a free key. The
MAX_ATTEMPTScap bounds the cost.
Validating several workers at once
If you fan out to several workers, add a node that POSTs their outputs to /v1/consensus (2 to 16 answers) and route on false_consensus_risk before the judge node. See stop false consensus.
FAQ
How do I add validation to a LangGraph agent?
Add a node that calls a validator after the node that produces output, store the result in state, and add a conditional edge whose router returns the next node name based on that result.
How do I stop a LangGraph retry loop from running forever?
Keep an attempts counter in state and have the router return the reject node once the counter reaches your cap. LangGraph also has a recursion limit, but an explicit cap gives you a clean rejected status.
What should the judge node return?
A partial state update. In the example it returns the full Screwtape response under verdict and the reasons under feedback.
Is there an official Screwtape LangGraph package?
No. Screwtape has no SDK. The node calls the HTTP API directly. Screwtape's own one-node LangGraph example is at https://screwtape.ai/integrations/langgraph/.
Sources
- LangGraph Graph API docs. https://docs.langchain.com/oss/python/langgraph/graph-api
- Screwtape.ai openapi.json and LangGraph integration (checked 2026-10-20)
